Skip to content

Commit e4f1636

Browse files
Bump Microsoft.Extensions.AI.OpenAI from 10.5.0 to 10.9.0 (#1282)
1 parent 1fcd70d commit e4f1636

3 files changed

Lines changed: 82 additions & 9 deletions

File tree

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
<PackageVersion Include="NuGet.Protocol" Version="7.9.0" />
5555
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
5656
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
57-
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.0" />
57+
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.9.0" />
5858
<PackageVersion Include="ModelContextProtocol" Version="2.1.0" />
5959
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="2.1.0" />
6060
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
@@ -64,7 +64,7 @@
6464
<PackageVersion Include="System.ClientModel" Version="1.15.0" />
6565
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
6666
<PackageVersion Include="Octokit" Version="14.0.0" />
67-
<PackageVersion Include="OpenAI" Version="2.10.0" />
67+
<PackageVersion Include="OpenAI" Version="2.12.0" />
6868
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
6969
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
7070
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />

EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public static IServiceCollection AddAzureOpenAIServices(
5353
services.AddSingleton(provider =>
5454
new AzureOpenAIClient(endpoint, credential));
5555

56+
// Register the resolved credential so AIChatService can build its ResponsesClient
57+
// directly, bypassing the binary-incompatible AzureOpenAIClient.GetResponsesClient().
58+
services.AddSingleton(credential);
59+
5660
services.AddAzureOpenAIChatCompletion(
5761
aiOptions.ChatDeploymentName,
5862
aiOptions.Endpoint,

EssentialCSharp.Chat.Shared/Services/AIChatService.cs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using Azure.AI.OpenAI;
1+
using Azure.Core;
22
using Microsoft.Extensions.Logging;
33
using Microsoft.Extensions.Options;
44
using ModelContextProtocol.Client;
55
using ModelContextProtocol.Protocol;
66
using OpenAI.Responses;
77
using System.ClientModel;
8+
using System.ClientModel.Primitives;
89
using System.Collections.Frozen;
910

1011
namespace EssentialCSharp.Chat.Common.Services;
@@ -15,7 +16,6 @@ namespace EssentialCSharp.Chat.Common.Services;
1516
public partial class AIChatService : IChatCompletionService
1617
{
1718
private readonly AIOptions _Options;
18-
private readonly AzureOpenAIClient _AzureClient;
1919
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
2020
private readonly ResponsesClient _ResponseClient;
2121
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@@ -25,21 +25,90 @@ public partial class AIChatService : IChatCompletionService
2525
public bool IsAvailable => true;
2626
public bool SupportsContextualSearch => true;
2727

28-
public AIChatService(IOptions<AIOptions> options, AISearchService searchService, AzureOpenAIClient azureClient, ILogger<AIChatService> logger)
28+
// The scope required for Azure OpenAI token auth via managed identity.
29+
private const string AzureCognitiveServicesScope = "https://cognitiveservices.azure.com/.default";
30+
31+
// The Azure OpenAI REST API version used by Azure.AI.OpenAI 2.9.0-beta.1.
32+
// AzureOpenAIClient.GetResponsesClient() is binary-incompatible with OpenAI 2.12.0
33+
// because OpenAI changed the ResponsesClient constructor from (ClientPipeline, OpenAIClientOptions)
34+
// to (ClientPipeline, ResponsesClientOptions). Until Azure.AI.OpenAI ships an update that
35+
// supports OpenAI 2.12+, we construct ResponsesClient directly using BearerTokenPolicy.
36+
private const string AzureApiVersion = "2025-04-01-preview";
37+
38+
public AIChatService(IOptions<AIOptions> options, AISearchService searchService, TokenCredential credential, ILogger<AIChatService> logger)
2939
{
3040
_Options = options.Value;
3141
_SearchService = searchService;
3242
_Logger = logger;
3343
_AllowedMcpTools = _Options.AllowedMcpTools.ToFrozenSet(StringComparer.Ordinal);
3444

35-
// Initialize Azure OpenAI client and get the Response Client from it
36-
_AzureClient = azureClient;
37-
3845
#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
39-
_ResponseClient = _AzureClient.GetResponsesClient();
46+
// Build an Azure-authenticated ResponsesClient directly, targeting the deployment endpoint.
47+
// The endpoint is: {AzureEndpoint}/openai/deployments/{ChatDeploymentName}
48+
// ResponsesClient appends "/responses" to produce the full Azure REST path.
49+
var deploymentEndpoint = new Uri(
50+
$"{_Options.Endpoint.TrimEnd('/')}/openai/deployments/{_Options.ChatDeploymentName}");
51+
52+
var responsesOptions = new ResponsesClientOptions { Endpoint = deploymentEndpoint };
53+
responsesOptions.AddPolicy(new ApiVersionPipelinePolicy(AzureApiVersion), PipelinePosition.PerCall);
54+
55+
var tokenProvider = new AzureCogServicesTokenProvider(credential);
56+
var bearerPolicy = new BearerTokenPolicy(tokenProvider, AzureCognitiveServicesScope);
57+
_ResponseClient = new ResponsesClient(bearerPolicy, responsesOptions);
4058
#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
4159
}
4260

61+
/// <summary>
62+
/// Adds the Azure api-version query parameter to every outgoing request.
63+
/// Required because the base ResponsesClient does not know the Azure API version;
64+
/// that concern was previously handled internally by AzureResponsesClient.
65+
/// </summary>
66+
private sealed class ApiVersionPipelinePolicy(string apiVersion) : PipelinePolicy
67+
{
68+
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
69+
{
70+
AppendApiVersion(message);
71+
ProcessNext(message, pipeline, currentIndex);
72+
}
73+
74+
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
75+
{
76+
AppendApiVersion(message);
77+
await ProcessNextAsync(message, pipeline, currentIndex);
78+
}
79+
80+
private void AppendApiVersion(PipelineMessage message)
81+
{
82+
var uri = message.Request.Uri?.ToString();
83+
if (uri is null || uri.Contains("api-version", StringComparison.OrdinalIgnoreCase))
84+
return;
85+
var separator = uri.Contains('?') ? '&' : '?';
86+
message.Request.Uri = new Uri(uri + separator + "api-version=" + apiVersion);
87+
}
88+
}
89+
90+
/// <summary>
91+
/// Bridges Azure.Core's <see cref="TokenCredential"/> into the System.ClientModel
92+
/// <see cref="AuthenticationTokenProvider"/> abstraction used by <see cref="BearerTokenPolicy"/>.
93+
/// </summary>
94+
private sealed class AzureCogServicesTokenProvider(TokenCredential credential) : AuthenticationTokenProvider
95+
{
96+
public override GetTokenOptions CreateTokenOptions(IReadOnlyDictionary<string, object> context)
97+
=> new(context);
98+
99+
public override AuthenticationToken GetToken(GetTokenOptions options, CancellationToken cancellationToken)
100+
{
101+
var token = credential.GetToken(new TokenRequestContext([AzureCognitiveServicesScope]), cancellationToken);
102+
return new AuthenticationToken(token.Token, "Bearer", token.ExpiresOn, null);
103+
}
104+
105+
public override async ValueTask<AuthenticationToken> GetTokenAsync(GetTokenOptions options, CancellationToken cancellationToken)
106+
{
107+
var token = await credential.GetTokenAsync(new TokenRequestContext([AzureCognitiveServicesScope]), cancellationToken);
108+
return new AuthenticationToken(token.Token, "Bearer", token.ExpiresOn, null);
109+
}
110+
}
111+
43112
/// <summary>
44113
/// Gets a single chat completion response with all optional features
45114
/// </summary>

0 commit comments

Comments
 (0)