// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
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;
using OpenAI;
using OpenAI.Responses;
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
///
/// Integration tests that start a web server and use the OpenAI Responses SDK client to verify protocol compatibility.
/// These tests validate both streaming and non-streaming request scenarios.
///
public sealed class OpenAIResponsesIntegrationTests : IAsyncDisposable
{
private WebApplication? _app;
private HttpClient? _httpClient;
public async ValueTask DisposeAsync()
{
this._httpClient?.Dispose();
if (this._app != null)
{
await this._app.DisposeAsync();
}
}
///
/// Verifies that streaming responses work correctly with the OpenAI SDK client.
///
[Fact]
public async Task CreateResponseStreaming_WithSimpleMessage_ReturnsStreamingUpdatesAsync()
{
// Arrange
const string AgentName = "streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "One Two Three";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Count to 3");
// Assert
List updates = [];
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
Assert.NotEmpty(updates);
// Verify we got various streaming update types
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
Assert.Contains(updates, u => u is StreamingResponseOutputTextDeltaUpdate);
// Verify content was received
string content = contentBuilder.ToString();
Assert.Equal(ExpectedResponse, content);
}
///
/// Verifies that non-streaming responses work correctly with the OpenAI SDK client.
///
[Fact]
public async Task CreateResponse_WithSimpleMessage_ReturnsCompleteResponseAsync()
{
// Arrange
const string AgentName = "non-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello! How can I help you today?";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Hello");
// Assert
Assert.NotNull(response);
Assert.Equal(ResponseStatus.Completed, response.Status);
Assert.NotNull(response.Id);
// Verify content
string content = response.GetOutputText();
Assert.Equal(ExpectedResponse, content);
}
///
/// Verifies that streaming responses can handle multiple content chunks.
///
[Fact]
public async Task CreateResponseStreaming_WithMultipleChunks_StreamsAllContentAsync()
{
// Arrange
const string AgentName = "multi-chunk-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "This is a test response with multiple words";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List updates = [];
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
// Verify all content was received
string receivedContent = contentBuilder.ToString();
Assert.Equal(ExpectedResponse, receivedContent);
// Verify multiple content chunks were received
List contentUpdates = updates.OfType().ToList();
Assert.True(contentUpdates.Count > 1, "Expected multiple content chunks in streaming response");
}
///
/// Verifies that multiple agents can be accessed via the same server.
///
[Fact]
public async Task CreateResponse_WithMultipleAgents_EachAgentRespondsCorrectlyAsync()
{
// Arrange
const string Agent1Name = "agent-one";
const string Agent1Instructions = "You are agent one.";
const string Agent1Response = "Response from agent one";
const string Agent2Name = "agent-two";
const string Agent2Instructions = "You are agent two.";
const string Agent2Response = "Response from agent two";
this._httpClient = await this.CreateTestServerWithMultipleAgentsAsync(
(Agent1Name, Agent1Instructions, Agent1Response),
(Agent2Name, Agent2Instructions, Agent2Response));
ResponsesClient responseClient1 = this.CreateResponseClient(Agent1Name);
ResponsesClient responseClient2 = this.CreateResponseClient(Agent2Name);
// Act
ResponseResult response1 = await responseClient1.CreateResponseAsync("test-model", "Hello");
ResponseResult response2 = await responseClient2.CreateResponseAsync("test-model", "Hello");
// Assert
string content1 = response1.GetOutputText();
string content2 = response2.GetOutputText();
Assert.Equal(Agent1Response, content1);
Assert.Equal(Agent2Response, content2);
Assert.NotEqual(content1, content2);
}
///
/// Verifies that streaming and non-streaming work correctly for the same agent.
///
[Fact]
public async Task CreateResponse_SameAgentStreamingAndNonStreaming_BothWorkCorrectlyAsync()
{
// Arrange
const string AgentName = "dual-mode-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "This is the response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act - Non-streaming
ResponseResult nonStreamingResponse = await responseClient.CreateResponseAsync("test-model", "Test");
// Act - Streaming
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
StringBuilder streamingContent = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
streamingContent.Append(textDelta.Delta);
}
}
// Assert
string nonStreamingContent = nonStreamingResponse.GetOutputText();
Assert.Equal(ExpectedResponse, nonStreamingContent);
Assert.Equal(ExpectedResponse, streamingContent.ToString());
}
///
/// Verifies that the response status is correctly set for completed responses.
///
[Fact]
public async Task CreateResponse_CompletedResponse_HasCorrectStatusAsync()
{
// Arrange
const string AgentName = "status-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Complete";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
// Assert
Assert.Equal(ResponseStatus.Completed, response.Status);
Assert.NotNull(response.Id);
Assert.Equal(ExpectedResponse, response.GetOutputText());
}
///
/// Verifies that streaming responses contain the expected event sequence.
///
[Fact]
public async Task CreateResponseStreaming_VerifyEventSequence_ContainsExpectedEventsAsync()
{
// Arrange
const string AgentName = "event-sequence-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Test response with multiple words";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
// Verify event sequence
Assert.NotEmpty(updates);
// First event should be created
Assert.IsType(updates[0]);
// Last event should be completed
StreamingResponseUpdate lastUpdate = updates[^1];
Assert.IsType(lastUpdate);
// Should contain text delta events in between
List textDeltas = updates.Where(u => u is StreamingResponseOutputTextDeltaUpdate).ToList();
Assert.NotEmpty(textDeltas);
}
///
/// Verifies that streaming responses properly handle empty responses.
///
[Fact]
public async Task CreateResponseStreaming_EmptyResponse_HandlesGracefullyAsync()
{
// Arrange
const string AgentName = "empty-response-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
// Should still receive created and completed events
Assert.NotEmpty(updates);
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
}
///
/// Verifies that non-streaming responses include proper metadata.
///
[Fact]
public async Task CreateResponse_IncludesMetadata_HasRequiredFieldsAsync()
{
// Arrange
const string AgentName = "metadata-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response with metadata";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
// Assert
Assert.NotNull(response.Id);
Assert.NotNull(response.Model);
Assert.NotEqual(default, response.CreatedAt);
Assert.Equal(ResponseStatus.Completed, response.Status);
}
///
/// Verifies that streaming responses handle very long text correctly.
///
[Fact]
public async Task CreateResponseStreaming_LongText_StreamsAllContentAsync()
{
// Arrange
const string AgentName = "long-text-agent";
const string Instructions = "You are a helpful assistant.";
string expectedResponse = string.Join(" ", Enumerable.Range(1, 100).Select(i => $"Word{i}"));
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, expectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Generate long text");
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
string receivedContent = contentBuilder.ToString();
Assert.Equal(expectedResponse, receivedContent);
}
///
/// Verifies that streaming responses properly track output indices.
///
[Fact]
public async Task CreateResponseStreaming_OutputIndices_AreConsistentAsync()
{
// Arrange
const string AgentName = "output-index-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Test output index";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List outputIndices = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputItemAddedUpdate itemAdded)
{
outputIndices.Add(itemAdded.OutputIndex);
}
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
outputIndices.Add(textDelta.OutputIndex);
}
}
// All output indices should be the same (first output)
Assert.NotEmpty(outputIndices);
Assert.All(outputIndices, index => Assert.Equal(0, index));
}
///
/// Verifies that streaming responses handle single-word responses correctly.
///
[Fact]
public async Task CreateResponseStreaming_SingleWord_StreamsCorrectlyAsync()
{
// Arrange
const string AgentName = "single-word-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
}
///
/// Verifies that streaming responses preserve special characters and formatting.
///
[Fact]
public async Task CreateResponseStreaming_SpecialCharacters_PreservesFormattingAsync()
{
// Arrange
const string AgentName = "special-chars-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello! How are you? I'm fine. 100% great!";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
}
///
/// Verifies that non-streaming responses handle special characters correctly.
///
[Fact]
public async Task CreateResponse_SpecialCharacters_PreservesContentAsync()
{
// Arrange
const string AgentName = "special-chars-nonstreaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Symbols: @#$%^&*() Quotes: \"Hello\" 'World' Unicode: 你好 🌍";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
// Assert
string content = response.GetOutputText();
Assert.Equal(ExpectedResponse, content);
}
///
/// Verifies that streaming responses include item IDs consistently.
///
[Fact]
public async Task CreateResponseStreaming_ItemIds_AreConsistentAsync()
{
// Arrange
const string AgentName = "item-id-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Testing item IDs";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List itemIds = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputItemAddedUpdate itemAdded)
{
itemIds.Add(itemAdded.Item.Id);
}
if (update is StreamingResponseOutputTextDeltaUpdate textDelta && !string.IsNullOrEmpty(textDelta.ItemId))
{
itemIds.Add(textDelta.ItemId);
}
}
// All item IDs should be the same within a single response
Assert.NotEmpty(itemIds);
Assert.All(itemIds, id => Assert.Equal(itemIds[0], id));
}
///
/// Verifies that multiple sequential non-streaming requests work correctly.
///
[Fact]
public async Task CreateResponse_MultipleSequentialRequests_AllSucceedAsync()
{
// Arrange
const string AgentName = "sequential-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act & Assert - Make 5 sequential requests
for (int i = 0; i < 5; i++)
{
ResponseResult response = await responseClient.CreateResponseAsync("test-model", $"Request {i}");
Assert.NotNull(response);
Assert.Equal(ResponseStatus.Completed, response.Status);
Assert.Equal(ExpectedResponse, response.GetOutputText());
}
}
///
/// Verifies that multiple sequential streaming requests work correctly.
///
[Fact]
public async Task CreateResponseStreaming_MultipleSequentialRequests_AllStreamCorrectlyAsync()
{
// Arrange
const string AgentName = "sequential-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Streaming response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act & Assert - Make 3 sequential streaming requests
for (int i = 0; i < 3; i++)
{
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", $"Request {i}");
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
}
}
///
/// Verifies that response IDs are unique across multiple requests.
///
[Fact]
public async Task CreateResponse_MultipleRequests_GenerateUniqueIdsAsync()
{
// Arrange
const string AgentName = "unique-id-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
List responseIds = [];
for (int i = 0; i < 10; i++)
{
ResponseResult response = await responseClient.CreateResponseAsync("test-model", $"Request {i}");
responseIds.Add(response.Id);
}
// Assert
Assert.Equal(10, responseIds.Count);
Assert.Equal(responseIds.Count, responseIds.Distinct().Count()); // All IDs should be unique
}
///
/// Verifies that streaming responses track sequence numbers correctly.
///
[Fact]
public async Task CreateResponseStreaming_SequenceNumbers_AreMonotonicallyIncreasingAsync()
{
// Arrange
const string AgentName = "sequence-number-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Test sequence numbers with multiple words";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List sequenceNumbers = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
sequenceNumbers.Add(update.SequenceNumber);
}
// Verify sequence numbers are monotonically increasing starting from 0
Assert.NotEmpty(sequenceNumbers);
Assert.Equal(0, sequenceNumbers[0]);
for (int i = 1; i < sequenceNumbers.Count; i++)
{
Assert.True(sequenceNumbers[i] > sequenceNumbers[i - 1], $"Sequence number {sequenceNumbers[i]} should be greater than {sequenceNumbers[i - 1]}");
}
}
///
/// Verifies that non-streaming responses have correct model information.
///
[Fact]
public async Task CreateResponse_ModelInformation_IsCorrectAsync()
{
// Arrange
const string AgentName = "model-info-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Test model info";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
// Assert
Assert.NotNull(response.Model);
Assert.NotEmpty(response.Model);
}
///
/// Verifies that streaming responses properly handle responses with punctuation.
///
[Fact]
public async Task CreateResponseStreaming_Punctuation_PreservesContentAsync()
{
// Arrange
const string AgentName = "punctuation-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Hello, world! How are you today? I'm doing well.";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
Assert.Equal(ExpectedResponse, contentBuilder.ToString());
}
///
/// Verifies that non-streaming responses work with very short input.
///
[Fact]
public async Task CreateResponse_ShortInput_ReturnsValidResponseAsync()
{
// Arrange
const string AgentName = "short-input-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "OK";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Hi");
// Assert
Assert.NotNull(response);
Assert.Equal(ResponseStatus.Completed, response.Status);
Assert.Equal(ExpectedResponse, response.GetOutputText());
}
///
/// Verifies that streaming responses contain content index information.
///
[Fact]
public async Task CreateResponseStreaming_ContentIndices_AreConsistentAsync()
{
// Arrange
const string AgentName = "content-index-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Test content indices";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List contentIndices = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentIndices.Add(textDelta.ContentIndex);
}
}
// All content indices should be the same for a single text response
Assert.NotEmpty(contentIndices);
Assert.All(contentIndices, index => Assert.Equal(0, index));
}
///
/// Verifies that non-streaming responses handle newlines correctly.
///
[Fact]
public async Task CreateResponse_Newlines_PreservesFormattingAsync()
{
// Arrange
const string AgentName = "newline-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Line 1\nLine 2\nLine 3";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test");
// Assert
string content = response.GetOutputText();
Assert.Equal(ExpectedResponse, content);
Assert.Contains("\n", content);
}
///
/// Verifies that streaming responses handle newlines correctly.
///
[Fact]
public async Task CreateResponseStreaming_Newlines_PreservesFormattingAsync()
{
// Arrange
const string AgentName = "newline-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "First line\nSecond line\nThird line";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
StringBuilder contentBuilder = new();
await foreach (StreamingResponseUpdate update in streamingResult)
{
if (update is StreamingResponseOutputTextDeltaUpdate textDelta)
{
contentBuilder.Append(textDelta.Delta);
}
}
string content = contentBuilder.ToString();
Assert.Equal(ExpectedResponse, content);
Assert.Contains("\n", content);
}
///
/// Verifies that responses with image content are properly handled in non-streaming mode.
///
[Fact]
public async Task CreateResponse_ImageContent_ReturnsCorrectlyAsync()
{
// Arrange
const string AgentName = "image-content-agent";
const string Instructions = "You are a helpful assistant.";
const string ImageUrl = "https://example.com/test-image.png";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.ImageContentMockChatClient(ImageUrl));
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Show me an image");
// Assert
Assert.NotNull(response);
Assert.Equal(ResponseStatus.Completed, response.Status);
Assert.NotNull(response.Id);
}
///
/// Verifies that responses with image content stream correctly.
///
[Fact]
public async Task CreateResponseStreaming_ImageContent_StreamsCorrectlyAsync()
{
// Arrange
const string AgentName = "image-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ImageUrl = "https://example.com/test-image.png";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.ImageContentMockChatClient(ImageUrl));
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Show me an image");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
Assert.NotEmpty(updates);
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
}
///
/// Verifies that responses with audio content are properly handled.
///
[Fact]
public async Task CreateResponse_AudioContent_ReturnsCorrectlyAsync()
{
// Arrange
const string AgentName = "audio-content-agent";
const string Instructions = "You are a helpful assistant.";
const string AudioData = "base64_audio_data_here";
const string Transcript = "This is the audio transcript";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.AudioContentMockChatClient(AudioData, Transcript));
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Generate audio");
// Assert
Assert.NotNull(response);
Assert.Equal(ResponseStatus.Completed, response.Status);
Assert.NotNull(response.Id);
}
///
/// Verifies that responses with audio content stream correctly.
///
[Fact]
public async Task CreateResponseStreaming_AudioContent_StreamsCorrectlyAsync()
{
// Arrange
const string AgentName = "audio-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string AudioData = "base64_audio_data";
const string Transcript = "Audio transcript";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.AudioContentMockChatClient(AudioData, Transcript));
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Generate audio");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
Assert.NotEmpty(updates);
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
}
///
/// Verifies that responses with function calls are properly handled.
///
[Fact]
public async Task CreateResponse_FunctionCall_ReturnsCorrectlyAsync()
{
// Arrange
const string AgentName = "function-call-agent";
const string Instructions = "You are a helpful assistant.";
const string FunctionName = "get_weather";
const string Arguments = "{\"location\":\"Seattle\"}";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments));
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "What's the weather?");
// Assert
Assert.NotNull(response);
Assert.NotNull(response.Id);
}
///
/// Verifies that responses with function calls stream correctly.
///
[Fact]
public async Task CreateResponseStreaming_FunctionCall_StreamsCorrectlyAsync()
{
// Arrange
const string AgentName = "function-call-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string FunctionName = "calculate";
const string Arguments = "{\"expression\":\"2+2\"}";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments));
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Calculate 2+2");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
Assert.NotEmpty(updates);
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
}
///
/// Verifies that responses with mixed content types are properly handled.
///
[Fact]
public async Task CreateResponse_MixedContent_ReturnsCorrectlyAsync()
{
// Arrange
const string AgentName = "mixed-content-agent";
const string Instructions = "You are a helpful assistant.";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.MixedContentMockChatClient());
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Show me various content");
// Assert
Assert.NotNull(response);
Assert.Equal(ResponseStatus.Completed, response.Status);
Assert.NotNull(response.Id);
}
///
/// Verifies that responses with mixed content types stream correctly.
///
[Fact]
public async Task CreateResponseStreaming_MixedContent_StreamsCorrectlyAsync()
{
// Arrange
const string AgentName = "mixed-streaming-agent";
const string Instructions = "You are a helpful assistant.";
this._httpClient = await this.CreateTestServerWithCustomClientAsync(
agentName: AgentName,
instructions: Instructions,
chatClient: new TestHelpers.MixedContentMockChatClient());
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Show me various content");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
Assert.NotEmpty(updates);
Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate);
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
// Should have multiple output item added events due to different content types
List itemAddedUpdates = updates.Where(u => u is StreamingResponseOutputItemAddedUpdate).ToList();
Assert.NotEmpty(itemAddedUpdates);
}
///
/// Verifies that streaming text content includes proper done events.
///
[Fact]
public async Task CreateResponseStreaming_TextDone_IncludesDoneEventAsync()
{
// Arrange
const string AgentName = "text-done-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Complete text response";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
// Should contain completed event (text done is represented by completed status)
Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate);
}
///
/// Verifies that content part added events are included in streaming responses.
///
[Fact]
public async Task CreateResponseStreaming_ContentPartAdded_IncludesEventAsync()
{
// Arrange
const string AgentName = "content-part-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response with content parts";
this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse);
ResponsesClient responseClient = this.CreateResponseClient(AgentName);
// Act
AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test");
// Assert
List updates = [];
await foreach (StreamingResponseUpdate update in streamingResult)
{
updates.Add(update);
}
// Should contain content part added event
Assert.Contains(updates, u => u is StreamingResponseContentPartAddedUpdate);
}
///
/// Verifies that when a client provides a conversation ID, the underlying IChatClient
/// does NOT receive that conversation ID via ChatOptions.ConversationId.
/// This ensures that the host's conversation management is separate from the IChatClient's
/// conversation handling (if any).
///
[Fact]
public async Task CreateResponse_WithConversationId_DoesNotForwardConversationIdToIChatClientAsync()
{
// Arrange
const string AgentName = "conversation-id-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Response";
this._httpClient = await this.CreateTestServerWithConversationsAsync(AgentName, Instructions, ExpectedResponse);
var mockChatClient = this.ResolveMockChatClient();
// First, create a conversation
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
string createConvJson = System.Text.Json.JsonSerializer.Serialize(createConversationRequest);
using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json");
HttpResponseMessage createConvResponse = await this._httpClient.PostAsync(
new Uri("/v1/conversations", UriKind.Relative),
createConvContent);
Assert.True(createConvResponse.IsSuccessStatusCode, $"Create conversation failed: {createConvResponse.StatusCode}");
string convResponseJson = await createConvResponse.Content.ReadAsStringAsync();
using var convDoc = System.Text.Json.JsonDocument.Parse(convResponseJson);
string conversationId = convDoc.RootElement.GetProperty("id").GetString()!;
// Act - Send request with conversation ID using raw HTTP
// (OpenAI SDK doesn't expose ConversationId directly on CreateResponseOptions)
var requestBody = new
{
input = "Test",
agent = new { name = AgentName },
conversation = conversationId,
stream = false
};
string requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody);
using StringContent content = new(requestJson, Encoding.UTF8, "application/json");
HttpResponseMessage httpResponse = await this._httpClient.PostAsync(
new Uri($"/{AgentName}/v1/responses", UriKind.Relative),
content);
// Assert - Response is successful
Assert.True(httpResponse.IsSuccessStatusCode, $"Response status: {httpResponse.StatusCode}");
// Assert - The IChatClient should have received ChatOptions, but without the ConversationId set
Assert.NotNull(mockChatClient.LastChatOptions);
Assert.Null(mockChatClient.LastChatOptions.ConversationId);
}
///
/// Verifies that when a client provides a conversation ID in streaming mode, the underlying
/// IChatClient does NOT receive that conversation ID via ChatOptions.ConversationId.
///
[Fact]
public async Task CreateResponseStreaming_WithConversationId_DoesNotForwardConversationIdToIChatClientAsync()
{
// Arrange
const string AgentName = "conversation-streaming-agent";
const string Instructions = "You are a helpful assistant.";
const string ExpectedResponse = "Streaming response";
this._httpClient = await this.CreateTestServerWithConversationsAsync(AgentName, Instructions, ExpectedResponse);
var mockChatClient = this.ResolveMockChatClient();
// First, create a conversation
var createConversationRequest = new { metadata = new { agent_id = AgentName } };
string createConvJson = System.Text.Json.JsonSerializer.Serialize(createConversationRequest);
using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json");
HttpResponseMessage createConvResponse = await this._httpClient.PostAsync(
new Uri("/v1/conversations", UriKind.Relative),
createConvContent);
Assert.True(createConvResponse.IsSuccessStatusCode, $"Create conversation failed: {createConvResponse.StatusCode}");
string convResponseJson = await createConvResponse.Content.ReadAsStringAsync();
using var convDoc = System.Text.Json.JsonDocument.Parse(convResponseJson);
string conversationId = convDoc.RootElement.GetProperty("id").GetString()!;
// Act - Send streaming request with conversation ID using raw HTTP
var requestBody = new
{
input = "Test",
agent = new { name = AgentName },
conversation = conversationId,
stream = true
};
string requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody);
using StringContent content = new(requestJson, Encoding.UTF8, "application/json");
HttpResponseMessage httpResponse = await this._httpClient.PostAsync(
new Uri($"/{AgentName}/v1/responses", UriKind.Relative),
content);
// Assert - Response is successful and is SSE
Assert.True(httpResponse.IsSuccessStatusCode, $"Response status: {httpResponse.StatusCode}");
Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType);
// Consume the SSE stream to complete the request
string sseContent = await httpResponse.Content.ReadAsStringAsync();
// Verify streaming completed successfully by checking for response.completed event
Assert.Contains("response.completed", sseContent);
// Assert - The IChatClient should have received ChatOptions, but without the ConversationId set
Assert.NotNull(mockChatClient.LastChatOptions);
Assert.Null(mockChatClient.LastChatOptions.ConversationId);
}
///
/// Verifies that conversation history is passed to the agent on subsequent requests.
/// This test reproduces the bug described in GitHub issue #3484.
///
[Fact]
public async Task CreateResponse_WithConversation_SecondRequestIncludesPriorMessagesAsync()
{
// Arrange
const string AgentName = "memory-agent";
const string Instructions = "You are a helpful assistant.";
const string AgentResponse = "Nice to meet you Alice";
var mockChatClient = new TestHelpers.ConversationMemoryMockChatClient(AgentResponse);
this._httpClient = await this.CreateTestServerWithCustomClientAndConversationsAsync(
AgentName, Instructions, mockChatClient);
// Create a conversation
string createConvJson = System.Text.Json.JsonSerializer.Serialize(
new { metadata = new { agent_id = AgentName } });
using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json");
HttpResponseMessage createConvResponse = await this._httpClient.PostAsync(
new Uri("/v1/conversations", UriKind.Relative), createConvContent);
Assert.True(createConvResponse.IsSuccessStatusCode);
string convJson = await createConvResponse.Content.ReadAsStringAsync();
using var convDoc = System.Text.Json.JsonDocument.Parse(convJson);
string conversationId = convDoc.RootElement.GetProperty("id").GetString()!;
// Act - First message
await this.SendRawResponseAsync(AgentName, "My name is Alice", conversationId, stream: false);
// Act - Second message in same conversation
await this.SendRawResponseAsync(AgentName, "What is my name?", conversationId, stream: false);
// Assert
Assert.Equal(2, mockChatClient.CallHistory.Count);
// First call: should have 1 message (just the user input)
Assert.Single(mockChatClient.CallHistory[0]);
Assert.Equal(ChatRole.User, mockChatClient.CallHistory[0][0].Role);
// Second call: should have 3 messages (prior user + prior assistant + new user)
Assert.Equal(3, mockChatClient.CallHistory[1].Count);
Assert.Equal(ChatRole.User, mockChatClient.CallHistory[1][0].Role);
Assert.Equal(ChatRole.Assistant, mockChatClient.CallHistory[1][1].Role);
Assert.Equal(ChatRole.User, mockChatClient.CallHistory[1][2].Role);
}
private async Task SendRawResponseAsync(
string agentName, string input, string conversationId, bool stream)
{
var requestBody = new
{
input,
agent = new { name = agentName },
conversation = conversationId,
stream
};
string json = System.Text.Json.JsonSerializer.Serialize(requestBody);
using StringContent content = new(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await this._httpClient!.PostAsync(
new Uri($"/{agentName}/v1/responses", UriKind.Relative), content);
Assert.True(response.IsSuccessStatusCode, $"Response failed: {response.StatusCode}");
// Consume the full response body to ensure execution completes
await response.Content.ReadAsStringAsync();
return response;
}
private ResponsesClient CreateResponseClient(string agentName)
{
return new ResponsesClient(
credential: new ApiKeyCredential("test-api-key"),
options: new OpenAIClientOptions
{
Endpoint = new Uri(this._httpClient!.BaseAddress!, $"/{agentName}/v1/"),
Transport = new HttpClientPipelineTransport(this._httpClient)
});
}
private TestHelpers.SimpleMockChatClient ResolveMockChatClient()
{
ArgumentNullException.ThrowIfNull(this._app, nameof(this._app));
var chatClient = this._app.Services.GetRequiredKeyedService("chat-client");
if (chatClient is not TestHelpers.SimpleMockChatClient mockChatClient)
{
throw new InvalidOperationException("Mock chat client not found or of incorrect type.");
}
return mockChatClient;
}
private async Task CreateTestServerAsync(string agentName, string instructions, string responseText = "Test response")
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddOpenAIResponses();
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName);
this._app.MapOpenAIResponses(agent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
private async Task CreateTestServerWithConversationsAsync(string agentName, string instructions, string responseText = "Test response")
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddOpenAIResponses();
builder.AddOpenAIConversations();
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName);
this._app.MapOpenAIResponses(agent);
this._app.MapOpenAIConversations();
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
private async Task CreateTestServerWithCustomClientAndConversationsAsync(string agentName, string instructions, IChatClient chatClient)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}");
builder.AddOpenAIResponses();
builder.AddOpenAIConversations();
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName);
this._app.MapOpenAIResponses(agent);
this._app.MapOpenAIConversations();
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
private async Task CreateTestServerWithCustomClientAsync(string agentName, string instructions, IChatClient chatClient)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient);
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}");
builder.AddOpenAIResponses();
this._app = builder.Build();
AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName);
this._app.MapOpenAIResponses(agent);
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
private async Task CreateTestServerWithMultipleAgentsAsync(
params (string Name, string Instructions, string ResponseText)[] agents)
{
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
foreach ((string name, string instructions, string responseText) in agents)
{
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
builder.Services.AddKeyedSingleton($"chat-client-{name}", mockChatClient);
builder.AddAIAgent(name, instructions, chatClientServiceKey: $"chat-client-{name}");
}
builder.AddOpenAIResponses();
this._app = builder.Build();
foreach ((string name, string _, string _) in agents)
{
AIAgent agent = this._app.Services.GetRequiredKeyedService(name);
this._app.MapOpenAIResponses(agent);
}
await this._app.StartAsync();
TestServer testServer = this._app.Services.GetRequiredService() as TestServer
?? throw new InvalidOperationException("TestServer not found");
return testServer.CreateClient();
}
}