Files
wehub-resource-sync db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:25 +08:00

249 lines
10 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
/// <summary>
/// Tests for <see cref="OpenAIResponsesMapOptions"/> and <see cref="OpenAIChatCompletionsMapOptions"/>,
/// including the default behavior that rejects request-supplied settings so that callers cannot
/// override the configuration of a self-contained agent.
/// </summary>
public sealed class OpenAIMapOptionsTests
{
[Fact]
public void Responses_DefaultRunOptionsFactory_IsRejectRequestSettings()
{
// Arrange & Act
var options = new OpenAIResponsesMapOptions();
// Assert
Assert.Equal(OpenAIResponsesMapOptions.RejectRequestSettings, options.RunOptionsFactory);
}
[Fact]
public void ChatCompletions_DefaultRunOptionsFactory_IsRejectRequestSettings()
{
// Arrange & Act
var options = new OpenAIChatCompletionsMapOptions();
// Assert
Assert.Equal(OpenAIChatCompletionsMapOptions.RejectRequestSettings, options.RunOptionsFactory);
}
[Fact]
public void Responses_RejectRequestSettings_ReturnsNull_WhenNoSettingsSpecified()
{
// Arrange
var request = new OpenAIResponseRequestInfo { Model = "gpt-4o" };
// Act
AgentRunOptions? result = OpenAIResponsesMapOptions.RejectRequestSettings(request);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData("temperature")]
[InlineData("top_p")]
[InlineData("max_output_tokens")]
[InlineData("instructions")]
[InlineData("tools")]
[InlineData("tool_choice")]
public void Responses_RejectRequestSettings_Throws_WhenSettingSpecified(string setting)
{
// Arrange
var request = setting switch
{
"temperature" => new OpenAIResponseRequestInfo { Temperature = 0.5 },
"top_p" => new OpenAIResponseRequestInfo { TopP = 0.5 },
"max_output_tokens" => new OpenAIResponseRequestInfo { MaxOutputTokens = 100 },
"instructions" => new OpenAIResponseRequestInfo { Instructions = "be brief" },
"tools" => new OpenAIResponseRequestInfo { Tools = [ParseElement("{}")] },
"tool_choice" => new OpenAIResponseRequestInfo { ToolChoice = ChatToolMode.None },
_ => throw new ArgumentOutOfRangeException(nameof(setting))
};
// Act & Assert
NotSupportedException ex = Assert.Throws<NotSupportedException>(() => OpenAIResponsesMapOptions.RejectRequestSettings(request));
Assert.Contains(setting, ex.Message, StringComparison.Ordinal);
}
[Fact]
public void ChatCompletions_RejectRequestSettings_ReturnsNull_WhenNoSettingsSpecified()
{
// Arrange
var request = new OpenAIChatCompletionRequestInfo { Model = "gpt-4o" };
// Act
AgentRunOptions? result = OpenAIChatCompletionsMapOptions.RejectRequestSettings(request);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData("temperature")]
[InlineData("top_p")]
[InlineData("max_completion_tokens")]
[InlineData("frequency_penalty")]
[InlineData("presence_penalty")]
[InlineData("seed")]
[InlineData("stop")]
[InlineData("response_format")]
[InlineData("tools")]
[InlineData("tool_choice")]
public void ChatCompletions_RejectRequestSettings_Throws_WhenSettingSpecified(string setting)
{
// Arrange
var request = setting switch
{
"temperature" => new OpenAIChatCompletionRequestInfo { Temperature = 0.5f },
"top_p" => new OpenAIChatCompletionRequestInfo { TopP = 0.5f },
"max_completion_tokens" => new OpenAIChatCompletionRequestInfo { MaxOutputTokens = 100 },
"frequency_penalty" => new OpenAIChatCompletionRequestInfo { FrequencyPenalty = 0.5f },
"presence_penalty" => new OpenAIChatCompletionRequestInfo { PresencePenalty = 0.5f },
"seed" => new OpenAIChatCompletionRequestInfo { Seed = 42 },
"stop" => new OpenAIChatCompletionRequestInfo { StopSequences = ["stop"] },
"response_format" => new OpenAIChatCompletionRequestInfo { ResponseFormat = ChatResponseFormat.Json },
"tools" => new OpenAIChatCompletionRequestInfo { Tools = [AIFunctionFactory.Create(() => "x", "f")] },
"tool_choice" => new OpenAIChatCompletionRequestInfo { ToolChoice = ChatToolMode.None },
_ => throw new ArgumentOutOfRangeException(nameof(setting))
};
// Act & Assert
NotSupportedException ex = Assert.Throws<NotSupportedException>(() => OpenAIChatCompletionsMapOptions.RejectRequestSettings(request));
Assert.Contains(setting, ex.Message, StringComparison.Ordinal);
}
[Fact]
public async Task Responses_DefaultEndpoint_RejectsRequestWithSettingsAsync()
{
// Arrange
using var app = await CreateResponsesServerAsync("reject-agent", mapOptions: null);
HttpClient client = GetClient(app);
// Act
HttpResponseMessage response = await client.PostAsync(
new Uri("/reject-agent/v1/responses", UriKind.Relative),
new StringContent("""{"input":"hello","temperature":0.5}""", Encoding.UTF8, "application/json"));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
string body = await response.Content.ReadAsStringAsync();
Assert.Contains("temperature", body, StringComparison.Ordinal);
}
[Fact]
public async Task Responses_ConfiguredEndpoint_HonorsRequestSettingsAsync()
{
// Arrange
using var app = await CreateResponsesServerAsync("map-agent", PermissiveMapOptions.Responses());
HttpClient client = GetClient(app);
// Act
HttpResponseMessage response = await client.PostAsync(
new Uri("/map-agent/v1/responses", UriKind.Relative),
new StringContent("""{"input":"hello","temperature":0.5}""", Encoding.UTF8, "application/json"));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task ChatCompletions_DefaultEndpoint_RejectsRequestWithSettingsAsync()
{
// Arrange
using var app = await CreateChatCompletionsServerAsync("reject-agent", mapOptions: null);
HttpClient client = GetClient(app);
// Act
HttpResponseMessage response = await client.PostAsync(
new Uri("/reject-agent/v1/chat/completions", UriKind.Relative),
new StringContent("""{"model":"myModel","messages":[{"role":"user","content":"hello"}],"temperature":0.5}""", Encoding.UTF8, "application/json"));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
string body = await response.Content.ReadAsStringAsync();
Assert.Contains("temperature", body, StringComparison.Ordinal);
Assert.Contains("invalid_request_error", body, StringComparison.Ordinal);
}
[Fact]
public async Task ChatCompletions_ConfiguredEndpoint_HonorsRequestSettingsAsync()
{
// Arrange
using var app = await CreateChatCompletionsServerAsync("map-agent", PermissiveMapOptions.ChatCompletions());
HttpClient client = GetClient(app);
// Act
HttpResponseMessage response = await client.PostAsync(
new Uri("/map-agent/v1/chat/completions", UriKind.Relative),
new StringContent("""{"model":"myModel","messages":[{"role":"user","content":"hello"}],"temperature":0.5}""", Encoding.UTF8, "application/json"));
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
private static async Task<WebApplication> CreateResponsesServerAsync(string agentName, OpenAIResponsesMapOptions? mapOptions)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient("Hello there.");
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, "You are a helpful assistant.", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
WebApplication app = builder.Build();
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>(agentName);
app.MapOpenAIResponses(agent, responsesPath: null, mapOptions);
await app.StartAsync();
return app;
}
private static HttpClient GetClient(WebApplication app)
{
TestServer testServer = app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
private static async Task<WebApplication> CreateChatCompletionsServerAsync(string agentName, OpenAIChatCompletionsMapOptions? mapOptions)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient("Hello there.");
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddAIAgent(agentName, "You are a helpful assistant.", chatClientServiceKey: "chat-client");
builder.AddOpenAIChatCompletions();
WebApplication app = builder.Build();
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>(agentName);
app.MapOpenAIChatCompletions(agent, path: null, mapOptions);
await app.StartAsync();
return app;
}
private static JsonElement ParseElement(string json)
{
using JsonDocument document = JsonDocument.Parse(json);
return document.RootElement.Clone();
}
}