| File: OpenAIRequestPoliciesTests.cs | |
| Project: ..\..\..\test\Libraries\Microsoft.Extensions.AI.OpenAI.Tests\Microsoft.Extensions.AI.OpenAI.Tests.csproj (Microsoft.Extensions.AI.OpenAI.Tests) |
// 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.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using OpenAI; using Xunit; #pragma warning disable OPENAI001 // Experimental OpenAI APIs #pragma warning disable MEAI001 // OpenAIRequestPolicies is experimental namespace Microsoft.Extensions.AI; public class OpenAIRequestPoliciesTests { [Fact] public void AddPolicy_NullPolicy_Throws() { var policies = new OpenAIRequestPolicies(); Assert.Throws<ArgumentNullException>("policy", () => policies.AddPolicy(null!)); } [Fact] public void GetService_OpenAIChatClient_ReturnsStableInstance() { IChatClient client = NewChatClient(); var first = client.GetService<OpenAIRequestPolicies>(); var second = client.GetService<OpenAIRequestPolicies>(); Assert.NotNull(first); Assert.Same(first, second); } [Fact] public void GetService_OpenAIResponseClient_ReturnsInstance() { IChatClient client = new OpenAIClient(new ApiKeyCredential("k")).GetResponsesClient().AsIChatClient("m"); Assert.NotNull(client.GetService<OpenAIRequestPolicies>()); } [Fact] public void GetService_OpenAIEmbeddingGenerator_ReturnsInstance() { IEmbeddingGenerator<string, Embedding<float>> generator = new OpenAIClient(new ApiKeyCredential("k")).GetEmbeddingClient("m").AsIEmbeddingGenerator(); Assert.NotNull(generator.GetService<OpenAIRequestPolicies>()); } [Fact] public void GetService_PerClientIsolation() { var openAi = new OpenAIClient(new ApiKeyCredential("k")); var policiesA = openAi.GetChatClient("m").AsIChatClient().GetService<OpenAIRequestPolicies>(); var policiesB = openAi.GetChatClient("m").AsIChatClient().GetService<OpenAIRequestPolicies>(); Assert.NotSame(policiesA, policiesB); } [Fact] public void GetService_ReachableThroughDecoratorChain() { IChatClient inner = NewChatClient(); var innerPolicies = inner.GetService<OpenAIRequestPolicies>(); using IChatClient pipeline = inner .AsBuilder() .UseFunctionInvocation() .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) .Build(); Assert.Same(innerPolicies, pipeline.GetService<OpenAIRequestPolicies>()); } [Fact] public async Task AddPolicy_CustomUserAgent_ReplacesMeaiHeader() { using var handler = new CapturingUserAgentHandler(); using var http = new HttpClient(handler); IChatClient client = NewChatClient(http); client.GetService<OpenAIRequestPolicies>()!.AddPolicy(new SetUserAgentPolicy("my-sdk/1.0")); await Assert.ThrowsAnyAsync<Exception>(() => client.GetResponseAsync("hi")); // Customer policy ran after MEAI's UA policy and replaced the value. Assert.NotNull(handler.CapturedUserAgent); Assert.Equal("my-sdk/1.0", handler.CapturedUserAgent); } [Fact] public async Task AddPolicy_CustomHeaderAdd_StacksWithMeaiUserAgent() { using var handler = new CapturingUserAgentHandler(); using var http = new HttpClient(handler); IChatClient client = NewChatClient(http); client.GetService<OpenAIRequestPolicies>()!.AddPolicy(new AddUserAgentPolicy("extra-sdk/9.9")); await Assert.ThrowsAnyAsync<Exception>(() => client.GetResponseAsync("hi")); Assert.NotNull(handler.CapturedUserAgent); Assert.Contains("MEAI", handler.CapturedUserAgent); Assert.Contains("extra-sdk/9.9", handler.CapturedUserAgent); } [Fact] public async Task NoPolicyRegistered_MeaiUserAgentStillEmitted() { using var handler = new CapturingUserAgentHandler(); using var http = new HttpClient(handler); IChatClient client = NewChatClient(http); Assert.NotNull(client.GetService<OpenAIRequestPolicies>()); // touch but don't register await Assert.ThrowsAnyAsync<Exception>(() => client.GetResponseAsync("hi")); Assert.NotNull(handler.CapturedUserAgent); Assert.Contains("MEAI", handler.CapturedUserAgent); } [Fact] public async Task AddPolicy_Concurrent_AllPoliciesRetained() { var policies = new OpenAIRequestPolicies(); const int Count = 200; await Task.WhenAll(Enumerable.Range(0, Count).Select(i => Task.Run(() => policies.AddPolicy(new NoopPolicy())))); // Verify nothing was lost across CAS races. var entries = (Array)typeof(OpenAIRequestPolicies) .GetField("_entries", BindingFlags.Instance | BindingFlags.NonPublic)! .GetValue(policies)!; Assert.Equal(Count, entries.Length); } private static IChatClient NewChatClient(HttpClient? http = null) { OpenAIClientOptions options = http is null ? new OpenAIClientOptions() : new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(http) }; return new OpenAIClient(new ApiKeyCredential("k"), options) .GetChatClient("gpt-4o-mini") .AsIChatClient(); } private sealed class CapturingUserAgentHandler : HttpMessageHandler { public string? CapturedUserAgent { get; private set; } protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { // Capture the User-Agent values exactly as they appear on the outgoing request. CapturedUserAgent = request.Headers.UserAgent.ToString(); // Short-circuit; the test only cares about what was sent. throw new InvalidOperationException("captured"); } } private sealed class NoopPolicy : PipelinePolicy { public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex) { ProcessNext(message, pipeline, currentIndex); } public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex) { return ProcessNextAsync(message, pipeline, currentIndex); } } private sealed class SetUserAgentPolicy : PipelinePolicy { private readonly string _value; public SetUserAgentPolicy(string value) { _value = value; } public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex) { message.Request.Headers.Set("User-Agent", _value); ProcessNext(message, pipeline, currentIndex); } public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex) { message.Request.Headers.Set("User-Agent", _value); return ProcessNextAsync(message, pipeline, currentIndex); } } private sealed class AddUserAgentPolicy : PipelinePolicy { private readonly string _value; public AddUserAgentPolicy(string value) { _value = value; } public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex) { message.Request.Headers.Add("User-Agent", _value); ProcessNext(message, pipeline, currentIndex); } public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex) { message.Request.Headers.Add("User-Agent", _value); return ProcessNextAsync(message, pipeline, currentIndex); } } }