diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml
index eb82e04d14..6b921a4e3b 100644
--- a/.github/workflows/dotnet-build-and-test.yml
+++ b/.github/workflows/dotnet-build-and-test.yml
@@ -127,7 +127,15 @@ jobs:
run: |
export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | tr '\n' ' ')
for project in $UT_PROJECTS; do
- dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute
+ # Query the project's target frameworks using MSBuild with the current configuration
+ target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
+
+ # Check if the project supports the target framework
+ if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
+ dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute
+ else
+ echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
+ fi
done
- name: Log event name and matrix integration-tests
@@ -148,7 +156,15 @@ jobs:
run: |
export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ')
for project in $INTEGRATION_TEST_PROJECTS; do
- dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
+ # Query the project's target frameworks using MSBuild with the current configuration
+ target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r')
+
+ # Check if the project supports the target framework
+ if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then
+ dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx
+ else
+ echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)"
+ fi
done
env:
# OpenAI Models
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index f01315c214..9a5ea670bf 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -22,16 +22,18 @@
+
+
+
+
-
-
@@ -93,6 +95,7 @@
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 1915050ac3..772f5031a1 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -277,6 +277,7 @@
+
diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
index 4eeb381585..8fce124db0 100644
--- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
+++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
@@ -6,7 +6,6 @@
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Hosting.A2A.AspNetCore;
-using Microsoft.Agents.AI.Hosting.OpenAI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
@@ -81,6 +80,7 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
builder.AddSequentialWorkflow("science-sequential-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
builder.AddConcurrentWorkflow("science-concurrent-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent();
+builder.AddOpenAIResponses();
var app = builder.Build();
@@ -102,16 +102,11 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te
// Url = "http://localhost:5390/a2a/knights-and-knaves"
});
-app.MapOpenAIResponses("pirate");
-app.MapOpenAIResponses("knights-and-knaves");
+app.MapOpenAIResponses();
app.MapOpenAIChatCompletions("pirate");
app.MapOpenAIChatCompletions("knights-and-knaves");
-// workflow-agents
-app.MapOpenAIResponses("science-sequential-workflow");
-app.MapOpenAIResponses("science-concurrent-workflow");
-
// Map the agents HTTP endpoints
app.MapAgentDiscovery("/agents");
diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs b/dotnet/samples/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs
index 524538bbf9..bb7f6c151c 100644
--- a/dotnet/samples/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs
+++ b/dotnet/samples/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs
@@ -23,11 +23,11 @@ public async override IAsyncEnumerable RunStreamingAsync
{
OpenAIClientOptions options = new()
{
- Endpoint = new Uri(httpClient.BaseAddress!, $"/{agentName}/v1/"),
+ Endpoint = new Uri(httpClient.BaseAddress!, "/v1/"),
Transport = new HttpClientPipelineTransport(httpClient)
};
- var openAiClient = new OpenAIResponseClient(model: "myModel!", credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
+ var openAiClient = new OpenAIResponseClient(model: agentName, credential: new ApiKeyCredential("dummy-key"), options: options).AsIChatClient();
var chatOptions = new ChatOptions()
{
ConversationId = threadId
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Utils/ChatCompletionsOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Utils/ChatCompletionsOptionsExtensions.cs
index 2816e015e0..36534c637c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Utils/ChatCompletionsOptionsExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Utils/ChatCompletionsOptionsExtensions.cs
@@ -12,8 +12,8 @@ namespace Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Utils;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Specifically for accessing hidden members")]
internal static class ChatCompletionsOptionsExtensions
{
- private static readonly Func _getStreamNullable;
- private static readonly Func> _getMessages;
+ private static readonly Func s_getStreamNullable;
+ private static readonly Func> s_getMessages;
static ChatCompletionsOptionsExtensions()
{
@@ -21,32 +21,32 @@ static ChatCompletionsOptionsExtensions()
// However, it does parse most of the interesting fields into internal properties of `ChatCompletionsOptions` object.
// --- Stream (internal bool? Stream { get; set; }) ---
- const string streamPropName = "Stream";
- var streamProp = typeof(ChatCompletionOptions).GetProperty(streamPropName, BindingFlags.Instance | BindingFlags.NonPublic)
- ?? throw new MissingMemberException(typeof(ChatCompletionOptions).FullName!, streamPropName);
- var streamGetter = streamProp.GetGetMethod(nonPublic: true) ?? throw new MissingMethodException($"{streamPropName} getter not found.");
+ const string StreamPropName = "Stream";
+ var streamProp = typeof(ChatCompletionOptions).GetProperty(StreamPropName, BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new MissingMemberException(typeof(ChatCompletionOptions).FullName!, StreamPropName);
+ var streamGetter = streamProp.GetGetMethod(nonPublic: true) ?? throw new MissingMethodException($"{StreamPropName} getter not found.");
- _getStreamNullable = streamGetter.CreateDelegate>();
+ s_getStreamNullable = streamGetter.CreateDelegate>();
// --- Messages (internal IList Messages { get; set; }) ---
- const string inputPropName = "Messages";
- var inputProp = typeof(ChatCompletionOptions).GetProperty(inputPropName, BindingFlags.Instance | BindingFlags.NonPublic)
- ?? throw new MissingMemberException(typeof(ChatCompletionOptions).FullName!, inputPropName);
+ const string InputPropName = "Messages";
+ var inputProp = typeof(ChatCompletionOptions).GetProperty(InputPropName, BindingFlags.Instance | BindingFlags.NonPublic)
+ ?? throw new MissingMemberException(typeof(ChatCompletionOptions).FullName!, InputPropName);
var inputGetter = inputProp.GetGetMethod(nonPublic: true)
- ?? throw new MissingMethodException($"{inputPropName} getter not found.");
+ ?? throw new MissingMethodException($"{InputPropName} getter not found.");
- _getMessages = inputGetter.CreateDelegate>>();
+ s_getMessages = inputGetter.CreateDelegate>>();
}
public static IList GetMessages(this ChatCompletionOptions options)
{
Throw.IfNull(options);
- return _getMessages(options);
+ return s_getMessages(options);
}
public static bool GetStream(this ChatCompletionOptions options)
{
Throw.IfNull(options);
- return _getStreamNullable(options) ?? false;
+ return s_getStreamNullable(options) ?? false;
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs
index b331bd8522..5fe39019fc 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs
@@ -5,16 +5,16 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
+using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions;
-using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using OpenAI.Chat;
-namespace Microsoft.Agents.AI.Hosting.OpenAI;
+namespace Microsoft.AspNetCore.Builder;
-public static partial class EndpointRouteBuilderExtensions
+public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExtensions
{
///
/// Maps OpenAI ChatCompletions API endpoints to the specified for the given .
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs
index 7e3e349f39..0b89340167 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs
@@ -1,90 +1,94 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.ClientModel.Primitives;
-using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
+using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
-using Microsoft.AspNetCore.Builder;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
-using OpenAI.Responses;
-namespace Microsoft.Agents.AI.Hosting.OpenAI;
+namespace Microsoft.AspNetCore.Builder;
///
/// Provides extension methods for mapping OpenAI capabilities to an .
///
-public static partial class EndpointRouteBuilderExtensions
+public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExtensions
{
///
/// Maps OpenAI Responses API endpoints to the specified for the given .
///
/// The to add the OpenAI Responses endpoints to.
- /// The name of the AI agent service registered in the dependency injection container. This name is used to resolve the instance from the keyed services.
+ /// The instance to map the OpenAI Responses endpoints for.
+ public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteBuilder endpoints, AIAgent agent) =>
+ MapOpenAIResponses(endpoints, agent, responsesPath: null);
+
+ ///
+ /// Maps OpenAI Responses API endpoints to the specified for the given .
+ ///
+ /// The to add the OpenAI Responses endpoints to.
+ /// The instance to map the OpenAI Responses endpoints for.
/// Custom route path for the responses endpoint.
- /// Custom route path for the conversations endpoint.
- public static void MapOpenAIResponses(
+ public static IEndpointConventionBuilder MapOpenAIResponses(
this IEndpointRouteBuilder endpoints,
- string agentName,
- [StringSyntax("Route")] string? responsesPath = null,
- [StringSyntax("Route")] string? conversationsPath = null)
+ AIAgent agent,
+ [StringSyntax("Route")] string? responsesPath)
{
ArgumentNullException.ThrowIfNull(endpoints);
- ArgumentNullException.ThrowIfNull(agentName);
- if (responsesPath is null || conversationsPath is null)
- {
- ValidateAgentName(agentName);
- }
-
- var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
+ ArgumentNullException.ThrowIfNull(agent);
+ ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent.Name));
+ ValidateAgentName(agent.Name);
- responsesPath ??= $"/{agentName}/v1/responses";
- var responsesRouteGroup = endpoints.MapGroup(responsesPath);
- MapResponses(responsesRouteGroup, agent);
-
- // Will be included once we obtain the API to operate with thread (conversation).
-
- // conversationsPath ??= $"/{agentName}/v1/conversations";
- // var conversationsRouteGroup = endpoints.MapGroup(conversationsPath);
- // MapConversations(conversationsRouteGroup, agent, loggerFactory);
+ responsesPath ??= $"/{agent.Name}/v1/responses";
+ var group = endpoints.MapGroup(responsesPath);
+ var endpointAgentName = agent.DisplayName;
+ group.MapPost("/", async ([FromBody] CreateResponse createResponse, CancellationToken cancellationToken)
+ => await AIAgentResponsesProcessor.CreateModelResponseAsync(agent, createResponse, cancellationToken).ConfigureAwait(false))
+ .WithName(endpointAgentName + "/CreateResponse");
+ return group;
}
- private static void MapResponses(IEndpointRouteBuilder routeGroup, AIAgent agent)
+ ///
+ /// Maps OpenAI Responses API endpoints to the specified .
+ ///
+ /// The to add the OpenAI Responses endpoints to.
+ public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteBuilder endpoints) =>
+ MapOpenAIResponses(endpoints, responsesPath: null);
+
+ ///
+ /// Maps OpenAI Responses API endpoints to the specified .
+ ///
+ /// The to add the OpenAI Responses endpoints to.
+ /// Custom route path for the responses endpoint.
+ public static IEndpointConventionBuilder MapOpenAIResponses(
+ this IEndpointRouteBuilder endpoints,
+ [StringSyntax("Route")] string? responsesPath)
{
- var endpointAgentName = agent.DisplayName;
- var responsesProcessor = new AIAgentResponsesProcessor(agent);
+ ArgumentNullException.ThrowIfNull(endpoints);
- routeGroup.MapPost("/", async (HttpContext requestContext, CancellationToken cancellationToken) =>
+ responsesPath ??= "/v1/responses";
+ var group = endpoints.MapGroup(responsesPath);
+ group.MapPost("/", async ([FromBody] CreateResponse createResponse, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
{
- var requestBinary = await BinaryData.FromStreamAsync(requestContext.Request.Body, cancellationToken).ConfigureAwait(false);
-
- var responseOptions = new ResponseCreationOptions();
- var responseOptionsJsonModel = responseOptions as IJsonModel;
- Debug.Assert(responseOptionsJsonModel is not null);
-
- responseOptions = responseOptionsJsonModel.Create(requestBinary, ModelReaderWriterOptions.Json);
- if (responseOptions is null)
+ // DevUI uses the 'model' field to specify the agent name.
+ var agentName = createResponse.Agent?.Name ?? createResponse.Model;
+ if (agentName is null)
{
- return Results.BadRequest("Invalid request payload.");
+ return Results.BadRequest("No 'agent.name' or 'model' specified in the request.");
}
- return await responsesProcessor.CreateModelResponseAsync(responseOptions, cancellationToken).ConfigureAwait(false);
- }).WithName(endpointAgentName + "/CreateResponse");
- }
-
-#pragma warning disable IDE0051 // Remove unused private members
- private static void MapConversations(IEndpointRouteBuilder routeGroup, AIAgent agent)
-#pragma warning restore IDE0051 // Remove unused private members
- {
- var endpointAgentName = agent.DisplayName;
- var conversationsProcessor = new AIAgentConversationsProcessor(agent);
+ var agent = serviceProvider.GetKeyedService(agentName);
+ if (agent is null)
+ {
+ return Results.NotFound($"Agent named '{agentName}' was not found.");
+ }
- routeGroup.MapGet("/{conversation_id}", (string conversationId, CancellationToken cancellationToken)
- => conversationsProcessor.GetConversationAsync(conversationId, cancellationToken)
- ).WithName(endpointAgentName + "/RetrieveConversation");
+ return await AIAgentResponsesProcessor.CreateModelResponseAsync(agent, createResponse, cancellationToken).ConfigureAwait(false);
+ }).WithName("CreateResponse");
+ return group;
}
private static void ValidateAgentName([NotNull] string agentName)
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/HostApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/HostApplicationBuilderExtensions.cs
new file mode 100644
index 0000000000..f4bfeb2578
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/HostApplicationBuilderExtensions.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Extension methods for to configure OpenAI Responses support.
+///
+public static class MicrosoftAgentAIHostingOpenAIHostApplicationBuilderExtensions
+{
+ ///
+ /// Adds support for exposing instances via OpenAI Responses.
+ ///
+ /// The to configure.
+ /// The for method chaining.
+ public static IHostApplicationBuilder AddOpenAIResponses(this IHostApplicationBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ builder.Services.AddOpenAIResponses();
+
+ return builder;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj
index 4fbef3aea7..ee5202c53f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj
@@ -3,7 +3,7 @@
$(ProjectsCoreTargetFrameworks)
$(ProjectsDebugCoreTargetFrameworks)
- $(NoWarn);IDE1006;IDE0130;NU1504;OPENAI001
+ $(NoWarn);OPENAI001
Microsoft.Agents.AI.Hosting.OpenAI
alpha
$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Generated
@@ -11,26 +11,28 @@
+
true
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentConversationsProcessor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentConversationsProcessor.cs
deleted file mode 100644
index b846d4c32c..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentConversationsProcessor.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
-
-internal sealed class AIAgentConversationsProcessor
-{
-#pragma warning disable IDE0052 // Remove unread private members
- private readonly AIAgent _aiAgent;
-#pragma warning restore IDE0052 // Remove unread private members
-
- public AIAgentConversationsProcessor(AIAgent aiAgent)
- {
- this._aiAgent = aiAgent ?? throw new ArgumentNullException(nameof(aiAgent));
- }
-
- public async Task GetConversationAsync(string conversationId, CancellationToken cancellationToken)
- {
- // TODO come back to it later
- throw new NotImplementedException();
- }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs
index 0eefa37f2c..5178abd8dc 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AIAgentResponsesProcessor.cs
@@ -1,77 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Buffers;
-using System.ClientModel.Primitives;
-using System.Collections.Generic;
-using System.Diagnostics;
+using System.Linq;
using System.Net.ServerSentEvents;
-using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Agents.AI;
-using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Model;
-using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
-using Microsoft.Extensions.AI;
-using OpenAI.Responses;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
///
-/// OpenAI Responses processor associated with a specific .
+/// OpenAI Responses processor for .
///
-internal sealed class AIAgentResponsesProcessor
+internal static class AIAgentResponsesProcessor
{
- private readonly AIAgent _agent;
-
- public AIAgentResponsesProcessor(AIAgent agent)
+ public static async Task CreateModelResponseAsync(AIAgent agent, CreateResponse request, CancellationToken cancellationToken)
{
- this._agent = agent ?? throw new ArgumentNullException(nameof(agent));
- }
-
- public async Task CreateModelResponseAsync(ResponseCreationOptions responseCreationOptions, CancellationToken cancellationToken)
- {
- var options = new OpenAIResponsesRunOptions();
- AgentThread? agentThread = null; // not supported to resolve from conversationId
-
- var inputItems = responseCreationOptions.GetInput();
- var chatMessages = inputItems.AsChatMessages();
+ ArgumentNullException.ThrowIfNull(agent);
- if (responseCreationOptions.GetStream())
+ var context = new AgentInvocationContext(idGenerator: IdGenerator.From(request));
+ if (request.Stream == true)
{
- return new OpenAIStreamingResponsesResult(this._agent, chatMessages);
+ return new StreamingResponse(agent, request, context);
}
- var agentResponse = await this._agent.RunAsync(chatMessages, agentThread, options, cancellationToken).ConfigureAwait(false);
- return new OpenAIResponseResult(agentResponse);
- }
-
- private sealed class OpenAIResponseResult(AgentRunResponse agentResponse) : IResult
- {
- public async Task ExecuteAsync(HttpContext httpContext)
- {
- // note: OpenAI SDK types provide their own serialization implementation
- // so we cant simply return IResult wrap for the typed-object.
- // instead writing to the response body can be done.
-
- var cancellationToken = httpContext.RequestAborted;
- var response = httpContext.Response;
-
- var chatResponse = agentResponse.AsChatResponse();
- var openAIResponse = chatResponse.AsOpenAIResponse();
- var openAIResponseJsonModel = openAIResponse as IJsonModel;
- Debug.Assert(openAIResponseJsonModel is not null);
-
- var writer = new Utf8JsonWriter(response.BodyWriter, new JsonWriterOptions { SkipValidation = false });
- openAIResponseJsonModel.Write(writer, ModelReaderWriterOptions.Json);
- await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
- }
+ var messages = request.Input.GetInputMessages().Select(i => i.ToChatMessage());
+ var response = await agent.RunAsync(messages, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return Results.Ok(response.ToResponse(request, context));
}
- private sealed class OpenAIStreamingResponsesResult(AIAgent agent, IEnumerable chatMessages) : IResult
+ private sealed class StreamingResponse(AIAgent agent, CreateResponse createResponse, AgentInvocationContext context) : IResult
{
public Task ExecuteAsync(HttpContext httpContext)
{
@@ -85,100 +46,20 @@ public Task ExecuteAsync(HttpContext httpContext)
response.Headers.ContentEncoding = "identity";
httpContext.Features.GetRequiredFeature().DisableBuffering();
+ var chatMessages = createResponse.Input.GetInputMessages().Select(i => i.ToChatMessage()).ToList();
+ var events = agent.RunStreamingAsync(chatMessages, cancellationToken: cancellationToken)
+ .ToStreamingResponseAsync(createResponse, context, cancellationToken)
+ .Select(static evt => new SseItem(evt, evt.Type));
return SseFormatter.WriteAsync(
- source: this.GetStreamingResponsesAsync(cancellationToken),
+ source: events,
destination: response.Body,
- itemFormatter: (sseItem, bufferWriter) =>
+ itemFormatter: static (sseItem, bufferWriter) =>
{
- var jsonTypeInfo = OpenAIResponsesJsonUtilities.DefaultOptions.GetTypeInfo(sseItem.Data.GetType());
- var json = JsonSerializer.SerializeToUtf8Bytes(sseItem.Data, jsonTypeInfo);
- bufferWriter.Write(json);
+ using var writer = new Utf8JsonWriter(bufferWriter);
+ JsonSerializer.Serialize(writer, sseItem.Data, ResponsesJsonContext.Default.StreamingResponseEvent);
+ writer.Flush();
},
cancellationToken);
}
-
- private async IAsyncEnumerable> GetStreamingResponsesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- var sequenceNumber = 1;
- var outputIndex = 1;
- AgentThread? agentThread = null;
-
- ResponseItem? lastResponseItem = null;
- OpenAIResponse? lastOpenAIResponse = null;
-
- await foreach (var update in agent.RunStreamingAsync(chatMessages, thread: agentThread, cancellationToken: cancellationToken).ConfigureAwait(false))
- {
- if (string.IsNullOrEmpty(update.ResponseId)
- && string.IsNullOrEmpty(update.MessageId)
- && update.Contents is not { Count: > 0 })
- {
- continue;
- }
-
- if (sequenceNumber == 1)
- {
- lastOpenAIResponse = update.AsChatResponse().AsOpenAIResponse();
-
- var responseCreated = new StreamingCreatedResponse(sequenceNumber++)
- {
- Response = lastOpenAIResponse
- };
- yield return new(responseCreated, responseCreated.Type);
- }
-
- if (update.Contents is not { Count: > 0 })
- {
- continue;
- }
-
- // to help convert the AIContent into OpenAI ResponseItem we pack it into the known "chatMessage"
- // and use existing convertion extension method
- var chatMessage = new ChatMessage(ChatRole.Assistant, update.Contents)
- {
- MessageId = update.MessageId,
- CreatedAt = update.CreatedAt,
- RawRepresentation = update.RawRepresentation
- };
-
- foreach (var openAIResponseItem in MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseItems([chatMessage]))
- {
- if (chatMessage.MessageId is not null)
- {
- openAIResponseItem.SetId(chatMessage.MessageId);
- }
-
- lastResponseItem = openAIResponseItem;
-
- var responseOutputItemAdded = new StreamingOutputItemAddedResponse(sequenceNumber++)
- {
- OutputIndex = outputIndex++,
- Item = openAIResponseItem
- };
- yield return new(responseOutputItemAdded, responseOutputItemAdded.Type);
- }
- }
-
- if (lastResponseItem is not null)
- {
- // we were streaming "response.output_item.added" before
- // so we should complete it now via "response.output_item.done"
- var responseOutputDoneAdded = new StreamingOutputItemDoneResponse(sequenceNumber++)
- {
- OutputIndex = outputIndex++,
- Item = lastResponseItem
- };
- yield return new(responseOutputDoneAdded, responseOutputDoneAdded.Type);
- }
-
- if (lastOpenAIResponse is not null)
- {
- // complete the whole streaming with the full response model
- var responseCompleted = new StreamingCompletedResponse(sequenceNumber++)
- {
- Response = lastOpenAIResponse
- };
- yield return new(responseCompleted, responseCompleted.Type);
- }
- }
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentInvocationContext.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentInvocationContext.cs
new file mode 100644
index 0000000000..6db6dbbc51
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentInvocationContext.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
+
+///
+/// Represents the context for an agent invocation.
+///
+/// The ID generator.
+/// The JSON serializer options. If not provided, default options will be used.
+internal sealed class AgentInvocationContext(IdGenerator idGenerator, JsonSerializerOptions? jsonSerializerOptions = null)
+{
+ ///
+ /// Gets the ID generator for this context.
+ ///
+ public IdGenerator IdGenerator { get; } = idGenerator;
+
+ ///
+ /// Gets the response ID.
+ ///
+ public string ResponseId => this.IdGenerator.ResponseId;
+
+ ///
+ /// Gets the conversation ID.
+ ///
+ public string ConversationId => this.IdGenerator.ConversationId;
+
+ ///
+ /// Gets the JSON serializer options.
+ ///
+ public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? ResponsesJsonSerializerOptions.Default;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs
new file mode 100644
index 0000000000..b276489d00
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs
@@ -0,0 +1,200 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
+
+///
+/// Extension methods for converting agent responses to Response models.
+///
+internal static class AgentRunResponseExtensions
+{
+ ///
+ /// Converts an AgentRunResponse to a Response model.
+ ///
+ /// The agent run response to convert.
+ /// The original create response request.
+ /// The agent invocation context.
+ /// A Response model.
+ public static Response ToResponse(
+ this AgentRunResponse agentRunResponse,
+ CreateResponse request,
+ AgentInvocationContext context)
+ {
+ List output = [];
+
+ // Add a reasoning item if reasoning is configured in the request
+ if (request.Reasoning != null)
+ {
+ output.Add(new ReasoningItemResource
+ {
+ Id = context.IdGenerator.GenerateReasoningId(),
+ Status = null
+ });
+ }
+
+ output.AddRange(agentRunResponse.Messages
+ .SelectMany(msg => msg.ToItemResource(context.IdGenerator, context.JsonSerializerOptions)));
+
+ return new Response
+ {
+ Id = context.ResponseId,
+ CreatedAt = (agentRunResponse.CreatedAt ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(),
+ Model = request.Agent?.Name ?? request.Model,
+ Status = ResponseStatus.Completed,
+ Agent = request.Agent?.ToAgentId(),
+ Conversation = request.Conversation ?? (context.ConversationId != null ? new ConversationReference { Id = context.ConversationId } : null),
+ Metadata = request.Metadata is IReadOnlyDictionary metadata ? new Dictionary(metadata) : [],
+ Instructions = request.Instructions,
+ Temperature = request.Temperature ?? 1.0,
+ TopP = request.TopP ?? 1.0,
+ Output = output,
+ Usage = agentRunResponse.Usage.ToResponseUsage(),
+ ParallelToolCalls = request.ParallelToolCalls ?? true,
+ Tools = [.. request.Tools ?? []],
+ ToolChoice = request.ToolChoice,
+ ServiceTier = request.ServiceTier ?? "default",
+ Store = request.Store ?? true,
+ PreviousResponseId = request.PreviousResponseId,
+ Reasoning = request.Reasoning,
+ Text = request.Text,
+ MaxOutputTokens = request.MaxOutputTokens,
+ Truncation = request.Truncation,
+#pragma warning disable CS0618 // Type or member is obsolete
+ User = request.User,
+#pragma warning restore CS0618 // Type or member is obsolete
+ PromptCacheKey = request.PromptCacheKey,
+ SafetyIdentifier = request.SafetyIdentifier,
+ TopLogprobs = request.TopLogprobs,
+ MaxToolCalls = request.MaxToolCalls,
+ Background = request.Background,
+ Prompt = request.Prompt,
+ Error = null
+ };
+ }
+
+ ///
+ /// Converts a ChatMessage to ItemResource objects.
+ ///
+ /// The chat message to convert.
+ /// The ID generator to use for creating IDs.
+ /// The JSON serializer options to use.
+ /// An enumerable of ItemResource objects.
+ public static IEnumerable ToItemResource(this ChatMessage message, IdGenerator idGenerator, JsonSerializerOptions jsonSerializerOptions)
+ {
+ IList contents = [];
+ foreach (var content in message.Contents)
+ {
+ switch (content)
+ {
+ case FunctionCallContent functionCallContent:
+ // message.Role == ChatRole.Assistant
+ yield return functionCallContent.ToFunctionToolCallItemResource(idGenerator.GenerateFunctionCallId(), jsonSerializerOptions);
+ break;
+ case FunctionResultContent functionResultContent:
+ // message.Role == ChatRole.Tool
+ yield return functionResultContent.ToFunctionToolCallOutputItemResource(
+ idGenerator.GenerateFunctionOutputId());
+ break;
+ default:
+ // message.Role == ChatRole.Assistant
+ if (ItemContentConverter.ToItemContent(content) is { } itemContent)
+ {
+ contents.Add(itemContent);
+ }
+
+ break;
+ }
+ }
+
+ if (contents.Count > 0)
+ {
+ yield return new ResponsesAssistantMessageItemResource
+ {
+ Id = idGenerator.GenerateMessageId(),
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = contents
+ };
+ }
+ }
+
+ ///
+ /// Converts FunctionCallContent to a FunctionToolCallItemResource.
+ ///
+ /// The function call content to convert.
+ /// The ID to assign to the resource.
+ /// The JSON serializer options to use.
+ /// A FunctionToolCallItemResource.
+ public static FunctionToolCallItemResource ToFunctionToolCallItemResource(
+ this FunctionCallContent functionCallContent,
+ string id,
+ JsonSerializerOptions jsonSerializerOptions)
+ {
+ return new FunctionToolCallItemResource
+ {
+ Id = id,
+ Status = FunctionToolCallItemResourceStatus.Completed,
+ CallId = functionCallContent.CallId,
+ Name = functionCallContent.Name,
+ Arguments = JsonSerializer.Serialize(functionCallContent.Arguments, jsonSerializerOptions.GetTypeInfo(typeof(IDictionary)))
+ };
+ }
+
+ ///
+ /// Converts FunctionResultContent to a FunctionToolCallOutputItemResource.
+ ///
+ /// The function result content to convert.
+ /// The ID to assign to the resource.
+ /// A FunctionToolCallOutputItemResource.
+ public static FunctionToolCallOutputItemResource ToFunctionToolCallOutputItemResource(
+ this FunctionResultContent functionResultContent,
+ string id)
+ {
+ var output = functionResultContent.Exception is not null
+ ? $"{functionResultContent.Exception.GetType().Name}(\"{functionResultContent.Exception.Message}\")"
+ : $"{functionResultContent.Result?.ToString() ?? "(null)"}";
+ return new FunctionToolCallOutputItemResource
+ {
+ Id = id,
+ Status = FunctionToolCallOutputItemResourceStatus.Completed,
+ CallId = functionResultContent.CallId,
+ Output = output
+ };
+ }
+
+ ///
+ /// Converts UsageDetails to ResponseUsage.
+ ///
+ /// The usage details to convert.
+ /// A ResponseUsage object with zeros if usage is null.
+ public static ResponseUsage ToResponseUsage(this UsageDetails? usage)
+ {
+ if (usage == null)
+ {
+ return ResponseUsage.Zero;
+ }
+
+ var cachedTokens = usage.AdditionalCounts?.TryGetValue("InputTokenDetails.CachedTokenCount", out var cachedInputToken) ?? false
+ ? (int)cachedInputToken
+ : 0;
+ var reasoningTokens =
+ usage.AdditionalCounts?.TryGetValue("OutputTokenDetails.ReasoningTokenCount", out var reasoningToken) ?? false
+ ? (int)reasoningToken
+ : 0;
+
+ return new ResponseUsage
+ {
+ InputTokens = (int)(usage.InputTokenCount ?? 0),
+ InputTokensDetails = new InputTokensDetails { CachedTokens = cachedTokens },
+ OutputTokens = (int)(usage.OutputTokenCount ?? 0),
+ OutputTokensDetails = new OutputTokensDetails { ReasoningTokens = reasoningTokens },
+ TotalTokens = (int)(usage.TotalTokenCount ?? 0)
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs
new file mode 100644
index 0000000000..fb4ea9a04a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseUpdateExtensions.cs
@@ -0,0 +1,195 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
+
+///
+/// Extension methods for .
+///
+internal static class AgentRunResponseUpdateExtensions
+{
+ ///
+ /// Converts a stream of to stream of .
+ ///
+ /// The agent run response updates.
+ /// The create response request.
+ /// The agent invocation context.
+ /// The cancellation token.
+ /// A stream of response events.
+ internal static async IAsyncEnumerable ToStreamingResponseAsync(
+ this IAsyncEnumerable updates,
+ CreateResponse request,
+ AgentInvocationContext context,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var seq = new SequenceNumber();
+ var createdAt = DateTimeOffset.UtcNow;
+ var latestUsage = ResponseUsage.Zero;
+ yield return new StreamingResponseCreated { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.InProgress) };
+ yield return new StreamingResponseInProgress { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.InProgress) };
+
+ var outputIndex = 0;
+ List items = [];
+ var updateEnumerator = updates.GetAsyncEnumerator(cancellationToken);
+ await using var _ = updateEnumerator.ConfigureAwait(false);
+
+ AgentRunResponseUpdate? previousUpdate = null;
+ StreamingEventGenerator? generator = null;
+ while (await updateEnumerator.MoveNextAsync().ConfigureAwait(false))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var update = updateEnumerator.Current;
+
+ if (!IsSameMessage(update, previousUpdate))
+ {
+ // Finalize the current generator when moving to a new message.
+ foreach (var evt in generator?.Complete() ?? [])
+ {
+ OnEvent(evt);
+ yield return evt;
+ }
+
+ generator = null;
+ outputIndex++;
+ previousUpdate = update;
+ }
+
+ using var contentEnumerator = update.Contents.GetEnumerator();
+ while (contentEnumerator.MoveNext())
+ {
+ var content = contentEnumerator.Current;
+
+ // Usage content is handled separately.
+ if (content is UsageContent usageContent && usageContent.Details != null)
+ {
+ latestUsage += usageContent.Details.ToResponseUsage();
+ continue;
+ }
+
+ // Create a new generator if there is no existing one or the existing one does not support the content.
+ if (generator?.IsSupported(content) != true)
+ {
+ // Finalize the current generator, if there is one.
+ foreach (var evt in generator?.Complete() ?? [])
+ {
+ OnEvent(evt);
+ yield return evt;
+ }
+
+ // Increment output index when switching generators
+ if (generator is not null)
+ {
+ outputIndex++;
+ }
+
+ // Create a new generator based on the content type.
+ generator = content switch
+ {
+ TextContent => new AssistantMessageEventGenerator(context.IdGenerator, seq, outputIndex),
+ TextReasoningContent => new TextReasoningContentEventGenerator(context.IdGenerator, seq, outputIndex),
+ FunctionCallContent => new FunctionCallEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
+ FunctionResultContent => new FunctionResultEventGenerator(context.IdGenerator, seq, outputIndex),
+ ErrorContent => new ErrorContentEventGenerator(context.IdGenerator, seq, outputIndex),
+ UriContent uriContent when uriContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
+ DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
+ DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentEventGenerator(context.IdGenerator, seq, outputIndex),
+ HostedFileContent => new HostedFileContentEventGenerator(context.IdGenerator, seq, outputIndex),
+ DataContent => new FileContentEventGenerator(context.IdGenerator, seq, outputIndex),
+ _ => null
+ };
+
+ // If no generator could be created, skip this content.
+ if (generator is null)
+ {
+ continue;
+ }
+ }
+
+ foreach (var evt in generator.ProcessContent(content))
+ {
+ OnEvent(evt);
+ yield return evt;
+ }
+ }
+ }
+
+ // Finalize the active generator.
+ foreach (var evt in generator?.Complete() ?? [])
+ {
+ OnEvent(evt);
+ yield return evt;
+ }
+
+ yield return new StreamingResponseCompleted { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.Completed, outputs: items) };
+
+ void OnEvent(StreamingResponseEvent evt)
+ {
+ if (evt is StreamingOutputItemDone itemDone)
+ {
+ items.Add(itemDone.Item);
+ }
+ }
+
+ Response CreateResponse(ResponseStatus status = ResponseStatus.Completed, IEnumerable? outputs = null)
+ {
+ return new Response
+ {
+ Id = context.ResponseId,
+ CreatedAt = createdAt.ToUnixTimeSeconds(),
+ Model = request.Agent?.Name ?? request.Model,
+ Status = status,
+ Agent = request.Agent?.ToAgentId(),
+ Conversation = request.Conversation ?? new ConversationReference { Id = context.ConversationId },
+ Metadata = request.Metadata != null ? new Dictionary(request.Metadata) : [],
+ Instructions = request.Instructions,
+ Temperature = request.Temperature ?? 1.0,
+ TopP = request.TopP ?? 1.0,
+ Output = outputs?.ToList() ?? [],
+ Usage = latestUsage,
+ ParallelToolCalls = request.ParallelToolCalls ?? true,
+ Tools = [.. request.Tools ?? []],
+ ToolChoice = request.ToolChoice,
+ ServiceTier = request.ServiceTier ?? "default",
+ Store = request.Store ?? true,
+ PreviousResponseId = request.PreviousResponseId,
+ Reasoning = request.Reasoning,
+ Text = request.Text,
+ MaxOutputTokens = request.MaxOutputTokens,
+ Truncation = request.Truncation,
+#pragma warning disable CS0618 // Type or member is obsolete
+ User = request.User,
+ PromptCacheKey = request.PromptCacheKey,
+#pragma warning restore CS0618 // Type or member is obsolete
+ SafetyIdentifier = request.SafetyIdentifier,
+ TopLogprobs = request.TopLogprobs,
+ MaxToolCalls = request.MaxToolCalls,
+ Background = request.Background,
+ Prompt = request.Prompt,
+ Error = null
+ };
+ }
+ }
+
+ private static bool IsSameMessage(AgentRunResponseUpdate? first, AgentRunResponseUpdate? second)
+ {
+ return IsSameValue(first?.MessageId, second?.MessageId)
+ && IsSameValue(first?.AuthorName, second?.AuthorName)
+ && IsSameRole(first?.Role, second?.Role);
+
+ static bool IsSameValue(string? str1, string? str2) =>
+ str1 is not { Length: > 0 } || str2 is not { Length: > 0 } || str1 == str2;
+
+ static bool IsSameRole(ChatRole? value1, ChatRole? value2) =>
+ !value1.HasValue || !value2.HasValue || value1.Value == value2.Value;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/AgentReferenceExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/AgentReferenceExtensions.cs
new file mode 100644
index 0000000000..426dc66dc6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/AgentReferenceExtensions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+
+///
+/// Extension methods for converting between model types.
+///
+internal static class AgentReferenceExtensions
+{
+ ///
+ /// Converts an AgentReference to an AgentId.
+ ///
+ /// The agent reference to convert.
+ /// An AgentId, or null if the agent reference is null.
+ public static AgentId? ToAgentId(this AgentReference? agent)
+ {
+ return agent == null
+ ? null
+ : new AgentId(
+ type: new AgentIdType(agent.Type),
+ name: agent.Name,
+ version: agent.Version ?? "latest");
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs
new file mode 100644
index 0000000000..cbaf7cb87b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemContentConverter.cs
@@ -0,0 +1,163 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+
+///
+/// Provides bidirectional conversion between and types.
+///
+internal static class ItemContentConverter
+{
+ ///
+ /// Converts to .
+ ///
+ /// The to convert.
+ /// An object, or null if the content cannot be converted.
+ public static AIContent? ToAIContent(ItemContent itemContent)
+ {
+ // Check if we already have the raw representation to avoid unnecessary conversion
+ if (itemContent.RawRepresentation is AIContent rawContent)
+ {
+ return rawContent;
+ }
+
+ AIContent? aiContent = itemContent switch
+ {
+ // Text content
+ ItemContentInputText inputText => new TextContent(inputText.Text),
+ ItemContentOutputText outputText => new TextContent(outputText.Text),
+
+ // Error/refusal content
+ ItemContentRefusal refusal => new ErrorContent(refusal.Refusal),
+
+ // Image content
+ ItemContentInputImage inputImage when !string.IsNullOrEmpty(inputImage.ImageUrl) =>
+ inputImage.ImageUrl!.StartsWith("data:", StringComparison.OrdinalIgnoreCase)
+ ? new DataContent(inputImage.ImageUrl, "image/*")
+ : new UriContent(inputImage.ImageUrl, "image/*"),
+ ItemContentInputImage inputImage when !string.IsNullOrEmpty(inputImage.FileId) =>
+ new HostedFileContent(inputImage.FileId!),
+
+ // File content
+ ItemContentInputFile inputFile when !string.IsNullOrEmpty(inputFile.FileId) =>
+ new HostedFileContent(inputFile.FileId!),
+ ItemContentInputFile inputFile when !string.IsNullOrEmpty(inputFile.FileData) =>
+ new DataContent(inputFile.FileData!, "application/octet-stream"),
+
+ // Audio content - map to DataContent with media type based on format
+ ItemContentInputAudio inputAudio =>
+ new DataContent(inputAudio.Data, inputAudio.Format?.ToUpperInvariant() switch
+ {
+ "MP3" => "audio/mpeg",
+ "WAV" => "audio/wav",
+ "OPUS" => "audio/opus",
+ "AAC" => "audio/aac",
+ "FLAC" => "audio/flac",
+ "PCM16" => "audio/pcm",
+ _ => "audio/*"
+ }),
+ ItemContentOutputAudio outputAudio =>
+ new DataContent(outputAudio.Data, "audio/*"),
+
+ _ => null
+ };
+
+ if (aiContent is not null)
+ {
+ // Add image detail to additional properties if present
+ if (itemContent is ItemContentInputImage { Detail: not null } image)
+ {
+ (aiContent.AdditionalProperties ??= [])["detail"] = image.Detail;
+ }
+
+ // Preserve the original as raw representation for round-tripping
+ aiContent.RawRepresentation = itemContent;
+ }
+
+ return aiContent;
+ }
+
+ ///
+ /// Converts to for output messages.
+ ///
+ /// The AI content to convert.
+ /// An object, or null if the content cannot be converted.
+ public static ItemContent? ToItemContent(AIContent content)
+ {
+ // Check if we already have the raw representation to avoid unnecessary conversion
+ if (content.RawRepresentation is ItemContent itemContent)
+ {
+ return itemContent;
+ }
+
+ ItemContent? result = content switch
+ {
+ TextContent textContent => new ItemContentOutputText { Text = textContent.Text ?? string.Empty, Annotations = [], Logprobs = [] },
+ TextReasoningContent reasoningContent => new ItemContentOutputText { Text = reasoningContent.Text ?? string.Empty, Annotations = [], Logprobs = [] },
+ ErrorContent errorContent => new ItemContentRefusal { Refusal = errorContent.Message ?? string.Empty },
+ UriContent uriContent when uriContent.HasTopLevelMediaType("image") =>
+ new ItemContentInputImage
+ {
+ ImageUrl = uriContent.Uri?.ToString(),
+ Detail = GetImageDetail(uriContent)
+ },
+ DataContent dataContent when dataContent.HasTopLevelMediaType("image") =>
+ new ItemContentInputImage
+ {
+ ImageUrl = dataContent.Uri,
+ Detail = GetImageDetail(dataContent)
+ },
+ HostedFileContent hostedFile =>
+ new ItemContentInputFile
+ {
+ FileId = hostedFile.FileId
+ },
+ DataContent fileData when !fileData.HasTopLevelMediaType("image") && !fileData.HasTopLevelMediaType("audio") =>
+ new ItemContentInputFile
+ {
+ FileData = fileData.Uri,
+ Filename = fileData.Name
+ },
+ DataContent audioData when audioData.HasTopLevelMediaType("audio") =>
+ new ItemContentInputAudio
+ {
+ Data = audioData.Uri,
+ Format = audioData.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase) ? "mp3" :
+ audioData.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase) ? "wav" :
+ audioData.MediaType.Equals("audio/opus", StringComparison.OrdinalIgnoreCase) ? "opus" :
+ audioData.MediaType.Equals("audio/aac", StringComparison.OrdinalIgnoreCase) ? "aac" :
+ audioData.MediaType.Equals("audio/flac", StringComparison.OrdinalIgnoreCase) ? "flac" :
+ audioData.MediaType.Equals("audio/pcm", StringComparison.OrdinalIgnoreCase) ? "pcm16" :
+ "mp3" // Default to mp3
+ },
+ // Other AIContent types (FunctionCallContent, FunctionResultContent, etc.)
+ // are handled separately in the Responses API as different ItemResource types, not ItemContent
+ _ => null
+ };
+
+ if (result is not null)
+ {
+ result.RawRepresentation = content;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Extracts the image detail level from 's additional properties.
+ ///
+ /// The to extract detail from.
+ /// The detail level as a string, or null if not present.
+ private static string? GetImageDetail(AIContent content)
+ {
+ if (content.AdditionalProperties?.TryGetValue("detail", out object? value) is true)
+ {
+ return value?.ToString();
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceConverter.cs
new file mode 100644
index 0000000000..2865de63a6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceConverter.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+
+///
+/// JSON converter for ItemResource that handles type discrimination.
+///
+[ExcludeFromCodeCoverage]
+internal sealed class ItemResourceConverter : JsonConverter
+{
+ private readonly ResponsesJsonContext _context;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ItemResourceConverter()
+ {
+ this._context = ResponsesJsonContext.Default;
+ }
+
+ public override ItemResource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // Clone the reader to peek at the JSON
+ Utf8JsonReader readerClone = reader;
+
+ // Read through the JSON to find the type property
+ string? type = null;
+
+ if (readerClone.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException("Expected start of object");
+ }
+
+ while (readerClone.Read())
+ {
+ if (readerClone.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ if (readerClone.TokenType == JsonTokenType.PropertyName)
+ {
+ string propertyName = readerClone.GetString()!;
+ readerClone.Read(); // Move to the value
+
+ if (propertyName == "type")
+ {
+ type = readerClone.GetString();
+ break;
+ }
+
+ if (readerClone.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
+ {
+ // Skip nested objects/arrays
+ readerClone.Skip();
+ }
+ }
+ }
+
+ // Determine the concrete type based on the type discriminator and deserialize using the source generation context
+ return type switch
+ {
+ ResponsesMessageItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesMessageItemResource),
+ FileSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FileSearchToolCallItemResource),
+ FunctionToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FunctionToolCallItemResource),
+ FunctionToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.FunctionToolCallOutputItemResource),
+ ComputerToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ComputerToolCallItemResource),
+ ComputerToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ComputerToolCallOutputItemResource),
+ WebSearchToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.WebSearchToolCallItemResource),
+ ReasoningItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ReasoningItemResource),
+ ItemReferenceItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ItemReferenceItemResource),
+ ImageGenerationToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.ImageGenerationToolCallItemResource),
+ CodeInterpreterToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.CodeInterpreterToolCallItemResource),
+ LocalShellToolCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.LocalShellToolCallItemResource),
+ LocalShellToolCallOutputItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.LocalShellToolCallOutputItemResource),
+ MCPListToolsItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPListToolsItemResource),
+ MCPApprovalRequestItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPApprovalRequestItemResource),
+ MCPApprovalResponseItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPApprovalResponseItemResource),
+ MCPCallItemResource.ItemType => JsonSerializer.Deserialize(ref reader, this._context.MCPCallItemResource),
+ _ => throw new JsonException($"Unknown item type: {type}")
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, ItemResource value, JsonSerializerOptions options)
+ {
+ // Directly serialize using the appropriate type info from the context
+ switch (value)
+ {
+ case ResponsesMessageItemResource message:
+ JsonSerializer.Serialize(writer, message, this._context.ResponsesMessageItemResource);
+ break;
+ case FileSearchToolCallItemResource fileSearch:
+ JsonSerializer.Serialize(writer, fileSearch, this._context.FileSearchToolCallItemResource);
+ break;
+ case FunctionToolCallItemResource functionCall:
+ JsonSerializer.Serialize(writer, functionCall, this._context.FunctionToolCallItemResource);
+ break;
+ case FunctionToolCallOutputItemResource functionOutput:
+ JsonSerializer.Serialize(writer, functionOutput, this._context.FunctionToolCallOutputItemResource);
+ break;
+ case ComputerToolCallItemResource computerCall:
+ JsonSerializer.Serialize(writer, computerCall, this._context.ComputerToolCallItemResource);
+ break;
+ case ComputerToolCallOutputItemResource computerOutput:
+ JsonSerializer.Serialize(writer, computerOutput, this._context.ComputerToolCallOutputItemResource);
+ break;
+ case WebSearchToolCallItemResource webSearch:
+ JsonSerializer.Serialize(writer, webSearch, this._context.WebSearchToolCallItemResource);
+ break;
+ case ReasoningItemResource reasoning:
+ JsonSerializer.Serialize(writer, reasoning, this._context.ReasoningItemResource);
+ break;
+ case ItemReferenceItemResource itemReference:
+ JsonSerializer.Serialize(writer, itemReference, this._context.ItemReferenceItemResource);
+ break;
+ case ImageGenerationToolCallItemResource imageGeneration:
+ JsonSerializer.Serialize(writer, imageGeneration, this._context.ImageGenerationToolCallItemResource);
+ break;
+ case CodeInterpreterToolCallItemResource codeInterpreter:
+ JsonSerializer.Serialize(writer, codeInterpreter, this._context.CodeInterpreterToolCallItemResource);
+ break;
+ case LocalShellToolCallItemResource localShell:
+ JsonSerializer.Serialize(writer, localShell, this._context.LocalShellToolCallItemResource);
+ break;
+ case LocalShellToolCallOutputItemResource localShellOutput:
+ JsonSerializer.Serialize(writer, localShellOutput, this._context.LocalShellToolCallOutputItemResource);
+ break;
+ case MCPListToolsItemResource mcpListTools:
+ JsonSerializer.Serialize(writer, mcpListTools, this._context.MCPListToolsItemResource);
+ break;
+ case MCPApprovalRequestItemResource mcpApprovalRequest:
+ JsonSerializer.Serialize(writer, mcpApprovalRequest, this._context.MCPApprovalRequestItemResource);
+ break;
+ case MCPApprovalResponseItemResource mcpApprovalResponse:
+ JsonSerializer.Serialize(writer, mcpApprovalResponse, this._context.MCPApprovalResponseItemResource);
+ break;
+ case MCPCallItemResource mcpCall:
+ JsonSerializer.Serialize(writer, mcpCall, this._context.MCPCallItemResource);
+ break;
+ default:
+ throw new JsonException($"Unknown item type: {value.GetType().Name}");
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemResourceConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemResourceConverter.cs
new file mode 100644
index 0000000000..c1045f9e56
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ResponsesMessageItemResourceConverter.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+
+///
+/// JSON converter for ResponsesMessageItemResource that handles nested type/role discrimination.
+///
+[ExcludeFromCodeCoverage]
+internal sealed class ResponsesMessageItemResourceConverter : JsonConverter
+{
+ private readonly ResponsesJsonContext _context;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ResponsesMessageItemResourceConverter()
+ {
+ this._context = ResponsesJsonContext.Default;
+ }
+
+ public override ResponsesMessageItemResource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // Clone the reader to peek at the JSON
+ Utf8JsonReader readerClone = reader;
+
+ // Read through the JSON to find the role property
+ string? role = null;
+
+ if (readerClone.TokenType != JsonTokenType.StartObject)
+ {
+ throw new JsonException("Expected start of object");
+ }
+
+ while (readerClone.Read())
+ {
+ if (readerClone.TokenType == JsonTokenType.EndObject)
+ {
+ break;
+ }
+
+ if (readerClone.TokenType == JsonTokenType.PropertyName)
+ {
+ string propertyName = readerClone.GetString()!;
+ readerClone.Read(); // Move to the value
+
+ if (propertyName == "role")
+ {
+ role = readerClone.GetString();
+ break;
+ }
+
+ if (readerClone.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray)
+ {
+ // Skip nested objects/arrays
+ readerClone.Skip();
+ }
+ }
+ }
+
+ // Determine the concrete type based on the role and deserialize using the source generation context
+ return role switch
+ {
+ ResponsesAssistantMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesAssistantMessageItemResource),
+ ResponsesUserMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesUserMessageItemResource),
+ ResponsesSystemMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesSystemMessageItemResource),
+ ResponsesDeveloperMessageItemResource.RoleType => JsonSerializer.Deserialize(ref reader, this._context.ResponsesDeveloperMessageItemResource),
+ _ => throw new JsonException($"Unknown message role: {role}")
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, ResponsesMessageItemResource value, JsonSerializerOptions options)
+ {
+ // Directly serialize using the appropriate type info from the context
+ switch (value)
+ {
+ case ResponsesAssistantMessageItemResource assistant:
+ JsonSerializer.Serialize(writer, assistant, this._context.ResponsesAssistantMessageItemResource);
+ break;
+ case ResponsesUserMessageItemResource user:
+ JsonSerializer.Serialize(writer, user, this._context.ResponsesUserMessageItemResource);
+ break;
+ case ResponsesSystemMessageItemResource system:
+ JsonSerializer.Serialize(writer, system, this._context.ResponsesSystemMessageItemResource);
+ break;
+ case ResponsesDeveloperMessageItemResource developer:
+ JsonSerializer.Serialize(writer, developer, this._context.ResponsesDeveloperMessageItemResource);
+ break;
+ default:
+ throw new JsonException($"Unknown message type: {value.GetType().Name}");
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/SnakeCaseEnumConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/SnakeCaseEnumConverter.cs
new file mode 100644
index 0000000000..fe8812330c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/SnakeCaseEnumConverter.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+
+///
+/// JSON converter for enums that uses snake_case naming convention.
+///
+/// The enum type to convert.
+[ExcludeFromCodeCoverage]
+internal sealed class SnakeCaseEnumConverter : JsonStringEnumConverter where T : struct, Enum
+{
+ public SnakeCaseEnumConverter() : base(JsonNamingPolicy.SnakeCaseLower)
+ {
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IdGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IdGenerator.cs
new file mode 100644
index 0000000000..63ec1a85bd
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/IdGenerator.cs
@@ -0,0 +1,170 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
+
+///
+/// Generates IDs with partition keys.
+///
+internal sealed partial class IdGenerator
+{
+ private readonly string _partitionId;
+
+#if NET9_0_OR_GREATER
+ [GeneratedRegex("^[A-Za-z0-9]+$")]
+ private static partial Regex WatermarkRegex();
+#else
+ private static readonly Regex s_watermarkRegex = new("^[A-Za-z0-9]+$", RegexOptions.Compiled);
+ private static Regex WatermarkRegex() => s_watermarkRegex;
+#endif
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The response ID.
+ /// The conversation ID.
+ public IdGenerator(string? responseId, string? conversationId)
+ {
+ this.ResponseId = responseId ?? NewId("resp");
+ this.ConversationId = conversationId ?? NewId("conv");
+ this._partitionId = GetPartitionIdOrDefault(this.ConversationId) ?? string.Empty;
+ }
+
+ ///
+ /// Creates a new ID generator from a create response request.
+ ///
+ /// The create response request.
+ /// A new ID generator.
+ public static IdGenerator From(CreateResponse request)
+ {
+ string? responseId = null;
+ request.Metadata?.TryGetValue("response_id", out responseId);
+ return new IdGenerator(responseId, request.Conversation?.Id);
+ }
+
+ ///
+ /// Gets the response ID.
+ ///
+ public string ResponseId { get; }
+
+ ///
+ /// Gets the conversation ID.
+ ///
+ public string ConversationId { get; }
+
+ ///
+ /// Generates a new ID.
+ ///
+ /// The optional category for the ID.
+ /// A generated ID string.
+ public string Generate(string? category = null)
+ {
+ var prefix = string.IsNullOrEmpty(category) ? "id" : category;
+ return NewId(prefix, partitionKey: this._partitionId);
+ }
+
+ ///
+ /// Generates a function call ID.
+ ///
+ /// A function call ID.
+ public string GenerateFunctionCallId() => this.Generate("func");
+
+ ///
+ /// Generates a function output ID.
+ ///
+ /// A function output ID.
+ public string GenerateFunctionOutputId() => this.Generate("funcout");
+
+ ///
+ /// Generates a message ID.
+ ///
+ /// A message ID.
+ public string GenerateMessageId() => this.Generate("msg");
+
+ ///
+ /// Generates a reasoning ID.
+ ///
+ /// A reasoning ID.
+ public string GenerateReasoningId() => this.Generate("rs");
+
+ ///
+ /// Generates a new ID with a structured format that includes a partition key.
+ ///
+ /// The prefix to add to the ID, typically indicating the resource type.
+ /// The length of the random entropy string in the ID.
+ /// The length of the partition key if generating a new one.
+ /// Optional additional text to insert between the prefix and the entropy.
+ /// Optional text to insert in the middle of the entropy string for traceability.
+ /// The delimiter character used to separate parts of the ID.
+ /// An explicit partition key to use. When provided, this value will be used instead of generating a new one.
+ /// An existing ID to extract the partition key from. When provided, the same partition key will be used instead of generating a new one.
+ /// A new ID with format "{prefix}{delimiter}{infix}{entropy}{delimiter}{partitionKey}".
+ /// Thrown when the watermark contains non-alphanumeric characters.
+ private static string NewId(string prefix, int stringLength = 32, int partitionKeyLength = 16, string infix = "",
+ string watermark = "", string delimiter = "_", string? partitionKey = null, string partitionKeyHint = "")
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(stringLength, 1);
+ var entropy = GetRandomString(stringLength);
+
+ string pKey = partitionKey ?? GetPartitionIdOrDefault(partitionKeyHint) ?? GetRandomString(partitionKeyLength);
+
+ if (!string.IsNullOrEmpty(watermark))
+ {
+ if (!WatermarkRegex().IsMatch(watermark))
+ {
+ throw new ArgumentException($"Only alphanumeric characters may be in watermark: {watermark}",
+ nameof(watermark));
+ }
+
+ entropy = $"{entropy[..(stringLength / 2)]}{watermark}{entropy[(stringLength / 2)..]}";
+ }
+
+ infix ??= "";
+ prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{delimiter}" : "";
+ return $"{prefix}{infix}{entropy}{pKey}";
+ }
+
+ ///
+ /// Generates a secure random alphanumeric string of the specified length.
+ ///
+ /// The desired length of the random string.
+ /// A random alphanumeric string.
+ /// Thrown when stringLength is less than 1.
+ private static string GetRandomString(int stringLength) =>
+ RandomNumberGenerator.GetString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", stringLength);
+
+ ///
+ /// Extracts the partition key from an existing ID, or returns null if extraction fails.
+ ///
+ /// The ID to extract the partition key from.
+ /// The length of the random entropy string in the ID.
+ /// The length of the partition key if generating a new one.
+ /// The delimiter character used in the ID.
+ /// The partition key if successfully extracted; otherwise, null.
+ private static string? GetPartitionIdOrDefault(string? id, int stringLength = 32, int partitionKeyLength = 16,
+ string delimiter = "_")
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ return null;
+ }
+
+ var parts = id.Split([delimiter], StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length < 2)
+ {
+ return null;
+ }
+
+ if (parts[1].Length < stringLength + partitionKeyLength)
+ {
+ return null;
+ }
+
+ // get last partitionKeyLength characters from the last part as the partition key
+ return parts[1][^partitionKeyLength..];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Model/ResponseStreamEvent.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Model/ResponseStreamEvent.cs
deleted file mode 100644
index b9bc0ed51c..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Model/ResponseStreamEvent.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Text.Json.Serialization;
-using OpenAI.Responses;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Model;
-
-///
-/// Abstract base class for all streaming response events in the OpenAI Responses API.
-/// Provides common properties shared across all streaming event types.
-///
-[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
-[JsonDerivedType(typeof(StreamingOutputItemAddedResponse), StreamingOutputItemAddedResponse.EventType)]
-[JsonDerivedType(typeof(StreamingOutputItemDoneResponse), StreamingOutputItemDoneResponse.EventType)]
-[JsonDerivedType(typeof(StreamingCreatedResponse), StreamingCreatedResponse.EventType)]
-[JsonDerivedType(typeof(StreamingCompletedResponse), StreamingCompletedResponse.EventType)]
-internal abstract class StreamingResponseEventBase
-{
- ///
- /// Gets or sets the type identifier for the streaming response event.
- /// This property is used to discriminate between different event types during serialization.
- ///
- [JsonPropertyName("type")]
- public string Type { get; set; }
-
- ///
- /// Gets or sets the sequence number of this event in the streaming response.
- /// Events are numbered sequentially starting from 1 to maintain ordering.
- ///
- [JsonPropertyName("sequence_number")]
- public int SequenceNumber { get; set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The type identifier for this streaming response event.
- /// The sequence number of this event in the streaming response.
- [JsonConstructor]
- public StreamingResponseEventBase(string type, int sequenceNumber)
- {
- this.Type = type;
- this.SequenceNumber = sequenceNumber;
- }
-}
-
-///
-/// Represents a streaming response event indicating that a new output item has been added to the response.
-/// This event is sent when the AI agent produces a new piece of content during streaming.
-///
-internal sealed class StreamingOutputItemAddedResponse : StreamingResponseEventBase
-{
- ///
- /// The constant event type identifier for output item added events.
- ///
- public const string EventType = "response.output_item.added";
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The sequence number of this event in the streaming response.
- public StreamingOutputItemAddedResponse(int sequenceNumber) : base(EventType, sequenceNumber)
- {
- }
-
- ///
- /// Gets or sets the index of the output in the response where this item was added.
- /// Multiple outputs can exist in a single response, and this identifies which one.
- ///
- [JsonPropertyName("output_index")]
- public int OutputIndex { get; set; }
-
- ///
- /// Gets or sets the response item that was added to the output.
- /// This contains the actual content or data produced by the AI agent.
- ///
- [JsonPropertyName("item")]
- public ResponseItem? Item { get; set; }
-}
-
-///
-/// Represents a streaming response event indicating that an output item has been completed.
-/// This event is sent when the AI agent finishes producing a particular piece of content.
-///
-internal sealed class StreamingOutputItemDoneResponse : StreamingResponseEventBase
-{
- ///
- /// The constant event type identifier for output item done events.
- ///
- public const string EventType = "response.output_item.done";
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The sequence number of this event in the streaming response.
- public StreamingOutputItemDoneResponse(int sequenceNumber) : base(EventType, sequenceNumber)
- {
- }
-
- ///
- /// Gets or sets the index of the output in the response where this item was completed.
- /// This corresponds to the same output index from the associated .
- ///
- [JsonPropertyName("output_index")]
- public int OutputIndex { get; set; }
-
- ///
- /// Gets or sets the completed response item.
- /// This contains the final version of the content produced by the AI agent.
- ///
- [JsonPropertyName("item")]
- public ResponseItem? Item { get; set; }
-}
-
-///
-/// Represents a streaming response event indicating that a new response has been created and streaming has begun.
-/// This is typically the first event sent in a streaming response sequence.
-///
-internal sealed class StreamingCreatedResponse : StreamingResponseEventBase
-{
- ///
- /// The constant event type identifier for response created events.
- ///
- public const string EventType = "response.created";
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The sequence number of this event in the streaming response.
- public StreamingCreatedResponse(int sequenceNumber) : base(EventType, sequenceNumber)
- {
- }
-
- ///
- /// Gets or sets the OpenAI response object that was created.
- /// This contains metadata about the response including ID, creation timestamp, and other properties.
- ///
- [JsonPropertyName("response")]
- public required OpenAIResponse Response { get; set; }
-}
-
-///
-/// Represents a streaming response event indicating that the response has been completed.
-/// This is typically the last event sent in a streaming response sequence.
-///
-internal sealed class StreamingCompletedResponse : StreamingResponseEventBase
-{
- ///
- /// The constant event type identifier for response completed events.
- ///
- public const string EventType = "response.completed";
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The sequence number of this event in the streaming response.
- public StreamingCompletedResponse(int sequenceNumber) : base(EventType, sequenceNumber)
- {
- }
-
- ///
- /// Gets or sets the completed OpenAI response object.
- /// This contains the final state of the response including all generated content and metadata.
- ///
- [JsonPropertyName("response")]
- public required OpenAIResponse Response { get; set; }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/AgentId.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/AgentId.cs
new file mode 100644
index 0000000000..11882ada6a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/AgentId.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Represents an agent identifier.
+///
+internal sealed record AgentId
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The agent ID type.
+ /// The name of the agent.
+ /// The version of the agent.
+ public AgentId(AgentIdType type, string name, string version)
+ {
+ this.Type = type;
+ this.Name = name;
+ this.Version = version;
+ }
+
+ ///
+ /// The agent ID type.
+ ///
+ [JsonPropertyName("type")]
+ public AgentIdType Type { get; init; }
+
+ ///
+ /// The name of the agent.
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; init; }
+
+ ///
+ /// The version of the agent.
+ ///
+ [JsonPropertyName("version")]
+ public string Version { get; init; }
+}
+
+///
+/// Represents an agent ID type.
+///
+internal sealed record AgentIdType
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type value.
+ public AgentIdType(string value)
+ {
+ this.Value = value;
+ }
+
+ ///
+ /// The type value.
+ ///
+ [JsonPropertyName("type")]
+ public string Value { get; init; }
+}
+
+///
+/// Represents an agent reference.
+///
+internal sealed record AgentReference
+{
+ ///
+ /// The type of the reference (e.g., "agent" or "agent_reference").
+ ///
+ [JsonPropertyName("type")]
+ public string Type { get; init; } = "agent_reference";
+
+ ///
+ /// The name of the agent.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; init; }
+
+ ///
+ /// The version of the agent.
+ ///
+ [JsonPropertyName("version")]
+ public string? Version { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ConversationReference.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ConversationReference.cs
new file mode 100644
index 0000000000..7f959fcbfc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ConversationReference.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Represents a reference to a conversation, which can be either a conversation ID (string) or a conversation object.
+///
+[JsonConverter(typeof(ConversationReferenceJsonConverter))]
+internal sealed record ConversationReference
+{
+ ///
+ /// The conversation ID.
+ ///
+ [JsonPropertyName("id")]
+ public string? Id { get; init; }
+
+ ///
+ /// The conversation metadata (optional, only when passing a conversation object).
+ ///
+ [JsonPropertyName("metadata")]
+ public Dictionary? Metadata { get; init; }
+
+ ///
+ /// Creates a conversation reference from a conversation ID.
+ ///
+ public static ConversationReference FromId(string id) => new() { Id = id };
+
+ ///
+ /// Creates a conversation reference from a conversation object.
+ ///
+ public static ConversationReference FromObject(string id, Dictionary? metadata = null) =>
+ new() { Id = id, Metadata = metadata };
+}
+
+///
+/// JSON converter for ConversationReference that handles both string (conversation ID) and object representations.
+///
+internal sealed class ConversationReferenceJsonConverter : JsonConverter
+{
+ public override ConversationReference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ // Handle string format: just the conversation ID
+ var id = reader.GetString();
+ return id is null ? null : ConversationReference.FromId(id);
+ }
+ else if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ // Handle object format: { "id": "...", "metadata": {...} }
+ using var doc = JsonDocument.ParseValue(ref reader);
+ var root = doc.RootElement;
+
+ var id = root.TryGetProperty("id", out var idProp) ? idProp.GetString() : null;
+ Dictionary? metadata = null;
+
+ if (root.TryGetProperty("metadata", out var metadataProp) && metadataProp.ValueKind == JsonValueKind.Object)
+ {
+ metadata = JsonSerializer.Deserialize(metadataProp.GetRawText(), ResponsesJsonContext.Default.DictionaryStringString);
+ }
+
+ return id is null ? null : ConversationReference.FromObject(id, metadata);
+ }
+ else if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ throw new JsonException($"Unexpected token type for ConversationReference: {reader.TokenType}");
+ }
+
+ public override void Write(Utf8JsonWriter writer, ConversationReference value, JsonSerializerOptions options)
+ {
+ if (value is null)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ // If only ID is present and no metadata, serialize as a simple string
+ if (value.Metadata is null || value.Metadata.Count == 0)
+ {
+ writer.WriteStringValue(value.Id);
+ }
+ else
+ {
+ // Otherwise, serialize as an object
+ writer.WriteStartObject();
+ writer.WriteString("id", value.Id);
+ if (value.Metadata is not null)
+ {
+ writer.WritePropertyName("metadata");
+ JsonSerializer.Serialize(writer, value.Metadata, ResponsesJsonContext.Default.DictionaryStringString);
+ }
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs
new file mode 100644
index 0000000000..99a5bbab73
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs
@@ -0,0 +1,194 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Request to create a model response.
+///
+internal sealed record CreateResponse
+{
+ ///
+ /// Text, image, or file inputs to the model, used to generate a response.
+ /// Can be either a simple string (equivalent to a user message) or an array of InputMessage objects.
+ ///
+ [JsonPropertyName("input")]
+ public required ResponseInput Input { get; init; }
+
+ ///
+ /// The agent to use for generating the response.
+ ///
+ [JsonPropertyName("agent")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public AgentReference? Agent { get; init; }
+
+ ///
+ /// Model used to generate the responses.
+ ///
+ [JsonPropertyName("model")]
+ public string? Model { get; init; }
+
+ ///
+ /// Inserts a system (or developer) message as the first item in the model's context.
+ ///
+ [JsonPropertyName("instructions")]
+ public string? Instructions { get; init; }
+
+ ///
+ /// An upper bound for the number of tokens that can be generated for a response,
+ /// including visible output tokens and reasoning tokens.
+ ///
+ [JsonPropertyName("max_output_tokens")]
+ public int? MaxOutputTokens { get; init; }
+
+ ///
+ /// Configuration options for reasoning models.
+ ///
+ [JsonPropertyName("reasoning")]
+ public ReasoningOptions? Reasoning { get; init; }
+
+ ///
+ /// Whether to store the generated model response for later retrieval via API.
+ ///
+ [JsonPropertyName("store")]
+ public bool? Store { get; init; }
+
+ ///
+ /// If set to true, the model response data will be streamed to the client as it is generated.
+ ///
+ [JsonPropertyName("stream")]
+ public bool? Stream { get; init; }
+
+ ///
+ /// The unique ID of the previous response to the model. Use this to create multi-turn conversations.
+ /// Cannot be used in conjunction with conversation.
+ ///
+ [JsonPropertyName("previous_response_id")]
+ public string? PreviousResponseId { get; init; }
+
+ ///
+ /// What sampling temperature to use, between 0 and 2.
+ ///
+ [JsonPropertyName("temperature")]
+ public double? Temperature { get; init; }
+
+ ///
+ /// An alternative to sampling with temperature, called nucleus sampling.
+ ///
+ [JsonPropertyName("top_p")]
+ public double? TopP { get; init; }
+
+ ///
+ /// Whether to allow the model to run tool calls in parallel.
+ ///
+ [JsonPropertyName("parallel_tool_calls")]
+ public bool? ParallelToolCalls { get; init; }
+
+ ///
+ /// Set of 16 key-value pairs that can be attached to an object.
+ ///
+ [JsonPropertyName("metadata")]
+ public Dictionary? Metadata { get; init; }
+
+ ///
+ /// Specify additional output data to include in the model response.
+ ///
+ [JsonPropertyName("include")]
+ public IReadOnlyList? Include { get; init; }
+
+ ///
+ /// The conversation that this response belongs to. Items from this conversation are prepended
+ /// to input_items for this response request.
+ /// Can be either a conversation ID (string) or a conversation object with ID and optional metadata.
+ /// Input items and output items from this response are automatically added to this conversation after this response completes.
+ ///
+ [JsonPropertyName("conversation")]
+ public ConversationReference? Conversation { get; init; }
+
+ ///
+ /// Whether to run the model response in the background.
+ ///
+ [JsonPropertyName("background")]
+ public bool? Background { get; init; }
+
+ ///
+ /// The maximum number of total calls to built-in tools that can be processed in a response.
+ ///
+ [JsonPropertyName("max_tool_calls")]
+ public int? MaxToolCalls { get; init; }
+
+ ///
+ /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position.
+ ///
+ [JsonPropertyName("top_logprobs")]
+ public int? TopLogprobs { get; init; }
+
+ ///
+ /// A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.
+ ///
+ [JsonPropertyName("safety_identifier")]
+ public string? SafetyIdentifier { get; init; }
+
+ ///
+ /// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates.
+ ///
+ [JsonPropertyName("prompt_cache_key")]
+ public string? PromptCacheKey { get; init; }
+
+ ///
+ /// Reference to a prompt template and its variables.
+ ///
+ [JsonPropertyName("prompt")]
+ public PromptReference? Prompt { get; init; }
+
+ ///
+ /// Specifies the processing type used for serving the request.
+ /// If set to 'auto', the request will be processed with the service tier configured in the Project settings.
+ /// If set to 'default', the request will be processed with standard pricing and performance.
+ /// If set to 'flex' or 'priority', the request will be processed with the corresponding service tier.
+ ///
+ [JsonPropertyName("service_tier")]
+ public string? ServiceTier { get; init; }
+
+ ///
+ /// Options for streaming responses. Only set this when you set stream: true.
+ ///
+ [JsonPropertyName("stream_options")]
+ public StreamOptions? StreamOptions { get; init; }
+
+ ///
+ /// The truncation strategy to use for the model response.
+ ///
+ [JsonPropertyName("truncation")]
+ public string? Truncation { get; init; }
+
+ ///
+ /// This field is being replaced by safety_identifier and prompt_cache_key.
+ /// Use prompt_cache_key instead to maintain caching optimizations.
+ ///
+ [JsonPropertyName("user")]
+ [Obsolete("This field is deprecated. Use safety_identifier and prompt_cache_key instead.")]
+ public string? User { get; init; }
+
+ ///
+ /// An array of tools the model may call while generating a response.
+ ///
+ [JsonPropertyName("tools")]
+ public IReadOnlyList? Tools { get; init; }
+
+ ///
+ /// How the model should select which tool (or tools) to use when generating a response.
+ ///
+ [JsonPropertyName("tool_choice")]
+ public JsonElement? ToolChoice { get; init; }
+
+ ///
+ /// Configuration options for a text response from the model. Can be plain text or structured JSON data.
+ ///
+ [JsonPropertyName("text")]
+ public TextConfiguration? Text { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessage.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessage.cs
new file mode 100644
index 0000000000..d15e111ebe
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessage.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// A message input to the model with a role indicating instruction following hierarchy.
+/// Aligns with the OpenAI Responses API InputMessage/EasyInputMessage schema.
+///
+internal sealed record InputMessage
+{
+ ///
+ /// The role of the message input. One of user, assistant, system, or developer.
+ ///
+ [JsonPropertyName("role")]
+ public required ChatRole Role { get; init; }
+
+ ///
+ /// Text, image, or audio input to the model, used to generate a response.
+ /// Can be a simple string or a list of content items with different types.
+ ///
+ [JsonPropertyName("content")]
+ public required InputMessageContent Content { get; init; }
+
+ ///
+ /// The type of the message input. Always "message".
+ ///
+ [JsonPropertyName("type")]
+ public string Type => "message";
+
+ ///
+ /// Converts this InputMessage to a ChatMessage.
+ ///
+ public ChatMessage ToChatMessage()
+ {
+ if (this.Content.IsText)
+ {
+ return new ChatMessage(this.Role, this.Content.Text!);
+ }
+ else if (this.Content.IsContents)
+ {
+ // Convert ItemContent to AIContent
+ var aiContents = this.Content.Contents!.Select(ItemContentConverter.ToAIContent).Where(c => c is not null).ToList();
+ return new ChatMessage(this.Role, aiContents!);
+ }
+
+ throw new InvalidOperationException("InputMessageContent has no value");
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessageContent.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessageContent.cs
new file mode 100644
index 0000000000..524613b7eb
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/InputMessageContent.cs
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Represents the content of an input message, which can be either a simple string or a list of ItemContent items.
+/// Aligns with the OpenAI typespec: string | InputContent[]
+///
+[JsonConverter(typeof(InputMessageContentJsonConverter))]
+internal sealed class InputMessageContent : IEquatable
+{
+ private InputMessageContent(string text)
+ {
+ this.Text = text ?? throw new ArgumentNullException(nameof(text));
+ this.Contents = null;
+ }
+
+ private InputMessageContent(IReadOnlyList contents)
+ {
+ this.Contents = contents ?? throw new ArgumentNullException(nameof(contents));
+ this.Text = null;
+ }
+
+ ///
+ /// Creates an InputMessageContent from a text string.
+ ///
+ public static InputMessageContent FromText(string text) => new(text);
+
+ ///
+ /// Creates an InputMessageContent from a list of ItemContent items.
+ ///
+ public static InputMessageContent FromContents(IReadOnlyList contents) => new(contents);
+
+ ///
+ /// Creates an InputMessageContent from a list of ItemContent items.
+ ///
+ public static InputMessageContent FromContents(params ItemContent[] contents) => new(contents);
+
+ ///
+ /// Implicit conversion from string to InputMessageContent.
+ ///
+ public static implicit operator InputMessageContent(string text) => FromText(text);
+
+ ///
+ /// Implicit conversion from ItemContent array to InputMessageContent.
+ ///
+ public static implicit operator InputMessageContent(ItemContent[] contents) => FromContents(contents);
+
+ ///
+ /// Implicit conversion from List to InputMessageContent.
+ ///
+ public static implicit operator InputMessageContent(List contents) => FromContents(contents);
+
+ ///
+ /// Gets whether this content is text.
+ ///
+ [MemberNotNullWhen(true, nameof(Text))]
+ public bool IsText => this.Text is not null;
+
+ ///
+ /// Gets whether this content is a list of ItemContent items.
+ ///
+ [MemberNotNullWhen(true, nameof(Contents))]
+ public bool IsContents => this.Contents is not null;
+
+ ///
+ /// Gets the text value, or null if this is not text content.
+ ///
+ public string? Text { get; }
+
+ ///
+ /// Gets the ItemContent items, or null if this is not a content list.
+ ///
+ public IReadOnlyList? Contents { get; }
+
+ ///
+ public bool Equals(InputMessageContent? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ // Both text
+ if (this.Text is not null && other.Text is not null)
+ {
+ return this.Text == other.Text;
+ }
+
+ // Both contents
+ if (this.Contents is not null && other.Contents is not null)
+ {
+ return this.Contents.SequenceEqual(other.Contents);
+ }
+
+ // One is text, one is contents - not equal
+ return false;
+ }
+
+ ///
+ public override bool Equals(object? obj) => this.Equals(obj as InputMessageContent);
+
+ ///
+ public override int GetHashCode()
+ {
+ if (this.Text is not null)
+ {
+ return this.Text.GetHashCode();
+ }
+
+ if (this.Contents is not null)
+ {
+ return this.Contents.Count > 0 ? this.Contents[0].GetHashCode() : 0;
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Equality operator.
+ ///
+ public static bool operator ==(InputMessageContent? left, InputMessageContent? right)
+ {
+ return Equals(left, right);
+ }
+
+ ///
+ /// Inequality operator.
+ ///
+ public static bool operator !=(InputMessageContent? left, InputMessageContent? right)
+ {
+ return !Equals(left, right);
+ }
+}
+
+///
+/// JSON converter for .
+///
+internal sealed class InputMessageContentJsonConverter : JsonConverter
+{
+ public override InputMessageContent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // Check if it's a string
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var text = reader.GetString();
+ return text is not null ? InputMessageContent.FromText(text) : null;
+ }
+
+ // Check if it's an array of ItemContent
+ if (reader.TokenType == JsonTokenType.StartArray)
+ {
+ var contents = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ListItemContent);
+ return contents?.Count > 0
+ ? InputMessageContent.FromContents(contents)
+ : InputMessageContent.FromText(string.Empty);
+ }
+
+ throw new JsonException($"Unexpected token type for InputMessageContent: {reader.TokenType}");
+ }
+
+ public override void Write(Utf8JsonWriter writer, InputMessageContent value, JsonSerializerOptions options)
+ {
+ if (value.IsText)
+ {
+ writer.WriteStringValue(value.Text);
+ }
+ else if (value.IsContents)
+ {
+ JsonSerializer.Serialize(writer, value.Contents, ResponsesJsonContext.Default.ListItemContent);
+ }
+ else
+ {
+ throw new JsonException("InputMessageContent has no value");
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs
new file mode 100644
index 0000000000..f0a7e83666
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ItemResource.cs
@@ -0,0 +1,696 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Base class for all item resources (output items from a response).
+///
+[JsonConverter(typeof(ItemResourceConverter))]
+internal abstract record ItemResource
+{
+ ///
+ /// The unique identifier for the item.
+ ///
+ [JsonPropertyName("id")]
+ public string Id { get; init; } = string.Empty;
+
+ ///
+ /// The type of the item.
+ ///
+ [JsonPropertyName("type")]
+ public abstract string Type { get; }
+}
+
+///
+/// Base class for message item resources.
+///
+[JsonConverter(typeof(ResponsesMessageItemResourceConverter))]
+internal abstract record ResponsesMessageItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for message items.
+ ///
+ public const string ItemType = "message";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the message.
+ ///
+ [JsonPropertyName("status")]
+ public ResponsesMessageItemResourceStatus Status { get; init; }
+
+ ///
+ /// The role of the message sender.
+ ///
+ [JsonPropertyName("role")]
+ public abstract ChatRole Role { get; }
+}
+
+///
+/// An assistant message item resource.
+///
+internal sealed record ResponsesAssistantMessageItemResource : ResponsesMessageItemResource
+{
+ ///
+ /// The constant role type identifier for assistant messages.
+ ///
+ public const string RoleType = "assistant";
+
+ ///
+ public override ChatRole Role => ChatRole.Assistant;
+
+ ///
+ /// The content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public required IList Content { get; init; }
+}
+
+///
+/// A user message item resource.
+///
+internal sealed record ResponsesUserMessageItemResource : ResponsesMessageItemResource
+{
+ ///
+ /// The constant role type identifier for user messages.
+ ///
+ public const string RoleType = "user";
+
+ ///
+ public override ChatRole Role => ChatRole.User;
+
+ ///
+ /// The content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public required IList Content { get; init; }
+}
+
+///
+/// A system message item resource.
+///
+internal sealed record ResponsesSystemMessageItemResource : ResponsesMessageItemResource
+{
+ ///
+ /// The constant role type identifier for system messages.
+ ///
+ public const string RoleType = "system";
+
+ ///
+ public override ChatRole Role => ChatRole.System;
+
+ ///
+ /// The content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public required IList Content { get; init; }
+}
+
+///
+/// A developer message item resource.
+///
+internal sealed record ResponsesDeveloperMessageItemResource : ResponsesMessageItemResource
+{
+ ///
+ /// The constant role type identifier for developer messages.
+ ///
+ public const string RoleType = "developer";
+
+ ///
+ public override ChatRole Role => new(RoleType);
+
+ ///
+ /// The content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public required IList Content { get; init; }
+}
+
+///
+/// A function tool call item resource.
+///
+internal sealed record FunctionToolCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for function call items.
+ ///
+ public const string ItemType = "function_call";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the function call.
+ ///
+ [JsonPropertyName("status")]
+ public FunctionToolCallItemResourceStatus Status { get; init; }
+
+ ///
+ /// The call ID of the function.
+ ///
+ [JsonPropertyName("call_id")]
+ public required string CallId { get; init; }
+
+ ///
+ /// The name of the function.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; init; }
+
+ ///
+ /// The arguments to the function.
+ ///
+ [JsonPropertyName("arguments")]
+ public required string Arguments { get; init; }
+}
+
+///
+/// A function tool call output item resource.
+///
+internal sealed record FunctionToolCallOutputItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for function call output items.
+ ///
+ public const string ItemType = "function_call_output";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the function call output.
+ ///
+ [JsonPropertyName("status")]
+ public FunctionToolCallOutputItemResourceStatus Status { get; init; }
+
+ ///
+ /// The call ID of the function.
+ ///
+ [JsonPropertyName("call_id")]
+ public required string CallId { get; init; }
+
+ ///
+ /// The output of the function.
+ ///
+ [JsonPropertyName("output")]
+ public required string Output { get; init; }
+}
+
+///
+/// The status of a message item resource.
+///
+[JsonConverter(typeof(SnakeCaseEnumConverter))]
+public enum ResponsesMessageItemResourceStatus
+{
+ ///
+ /// The message is completed.
+ ///
+ Completed,
+
+ ///
+ /// The message is in progress.
+ ///
+ InProgress,
+
+ ///
+ /// The message is incomplete.
+ ///
+ Incomplete
+}
+
+///
+/// The status of a function tool call item resource.
+///
+[JsonConverter(typeof(SnakeCaseEnumConverter))]
+public enum FunctionToolCallItemResourceStatus
+{
+ ///
+ /// The function call is completed.
+ ///
+ Completed,
+
+ ///
+ /// The function call is in progress.
+ ///
+ InProgress
+}
+
+///
+/// The status of a function tool call output item resource.
+///
+[JsonConverter(typeof(SnakeCaseEnumConverter))]
+public enum FunctionToolCallOutputItemResourceStatus
+{
+ ///
+ /// The function call output is completed.
+ ///
+ Completed
+}
+
+///
+/// Base class for item content.
+///
+[JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
+[JsonDerivedType(typeof(ItemContentInputText), "input_text")]
+[JsonDerivedType(typeof(ItemContentInputAudio), "input_audio")]
+[JsonDerivedType(typeof(ItemContentInputImage), "input_image")]
+[JsonDerivedType(typeof(ItemContentInputFile), "input_file")]
+[JsonDerivedType(typeof(ItemContentOutputText), "output_text")]
+[JsonDerivedType(typeof(ItemContentOutputAudio), "output_audio")]
+[JsonDerivedType(typeof(ItemContentRefusal), "refusal")]
+internal abstract record ItemContent
+{
+ ///
+ /// The type of the content.
+ ///
+ [JsonIgnore]
+ public abstract string Type { get; }
+
+ ///
+ /// Gets or sets the original representation of the content, if applicable.
+ /// This property is not serialized and is used for round-tripping conversions.
+ ///
+ [JsonIgnore]
+ public object? RawRepresentation { get; set; }
+}
+
+///
+/// Text input content.
+///
+internal sealed record ItemContentInputText : ItemContent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "input_text";
+
+ ///
+ /// The text content.
+ ///
+ [JsonPropertyName("text")]
+ public required string Text { get; init; }
+}
+
+///
+/// Audio input content.
+///
+internal sealed record ItemContentInputAudio : ItemContent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "input_audio";
+
+ ///
+ /// Base64-encoded audio data.
+ ///
+ [JsonPropertyName("data")]
+ public required string Data { get; init; }
+
+ ///
+ /// The format of the audio data.
+ ///
+ [JsonPropertyName("format")]
+ public required string Format { get; init; }
+}
+
+///
+/// Image input content.
+///
+internal sealed record ItemContentInputImage : ItemContent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "input_image";
+
+ ///
+ /// The URL of the image to be sent to the model. A fully qualified URL or base64 encoded image in a data URL.
+ ///
+ [JsonPropertyName("image_url")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:URI-like properties should not be strings", Justification = "OpenAI API uses string for image_url")]
+ public string? ImageUrl { get; init; }
+
+ ///
+ /// The ID of the file to be sent to the model.
+ ///
+ [JsonPropertyName("file_id")]
+ public string? FileId { get; init; }
+
+ ///
+ /// The detail level of the image to be sent to the model. One of 'high', 'low', or 'auto'. Defaults to 'auto'.
+ ///
+ [JsonPropertyName("detail")]
+ public string? Detail { get; init; }
+}
+
+///
+/// File input content.
+///
+internal sealed record ItemContentInputFile : ItemContent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "input_file";
+
+ ///
+ /// The ID of the file to be sent to the model.
+ ///
+ [JsonPropertyName("file_id")]
+ public string? FileId { get; init; }
+
+ ///
+ /// The name of the file to be sent to the model.
+ ///
+ [JsonPropertyName("filename")]
+ public string? Filename { get; init; }
+
+ ///
+ /// The content of the file to be sent to the model.
+ ///
+ [JsonPropertyName("file_data")]
+ public string? FileData { get; init; }
+}
+
+///
+/// Text output content.
+///
+internal sealed record ItemContentOutputText : ItemContent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "output_text";
+
+ ///
+ /// The text content.
+ ///
+ [JsonPropertyName("text")]
+ public required string Text { get; init; }
+
+ ///
+ /// The annotations.
+ ///
+ [JsonPropertyName("annotations")]
+ public required IList Annotations { get; init; }
+
+ ///
+ /// Log probability information for the output tokens.
+ ///
+ [JsonPropertyName("logprobs")]
+ public IList Logprobs { get; init; } = [];
+}
+
+///
+/// Audio output content.
+///
+internal sealed record ItemContentOutputAudio : ItemContent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "output_audio";
+
+ ///
+ /// Base64-encoded audio data from the model.
+ ///
+ [JsonPropertyName("data")]
+ public required string Data { get; init; }
+
+ ///
+ /// The transcript of the audio data from the model.
+ ///
+ [JsonPropertyName("transcript")]
+ public required string Transcript { get; init; }
+}
+
+///
+/// Refusal content.
+///
+internal sealed record ItemContentRefusal : ItemContent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "refusal";
+
+ ///
+ /// The refusal explanation from the model.
+ ///
+ [JsonPropertyName("refusal")]
+ public required string Refusal { get; init; }
+}
+
+// Additional ItemResource types from TypeSpec
+
+///
+/// A file search tool call item resource.
+///
+internal sealed record FileSearchToolCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for file search call items.
+ ///
+ public const string ItemType = "file_search_call";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the file search.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// A computer tool call item resource.
+///
+internal sealed record ComputerToolCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for computer call items.
+ ///
+ public const string ItemType = "computer_call";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the computer call.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// A computer tool call output item resource.
+///
+internal sealed record ComputerToolCallOutputItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for computer call output items.
+ ///
+ public const string ItemType = "computer_call_output";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the computer call output.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// A web search tool call item resource.
+///
+internal sealed record WebSearchToolCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for web search call items.
+ ///
+ public const string ItemType = "web_search_call";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the web search.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// A reasoning item resource.
+///
+internal sealed record ReasoningItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for reasoning items.
+ ///
+ public const string ItemType = "reasoning";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the reasoning.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// An item reference item resource.
+///
+internal sealed record ItemReferenceItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for item reference items.
+ ///
+ public const string ItemType = "item_reference";
+
+ ///
+ public override string Type => ItemType;
+}
+
+///
+/// An image generation tool call item resource.
+///
+internal sealed record ImageGenerationToolCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for image generation call items.
+ ///
+ public const string ItemType = "image_generation_call";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the image generation.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// A code interpreter tool call item resource.
+///
+internal sealed record CodeInterpreterToolCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for code interpreter call items.
+ ///
+ public const string ItemType = "code_interpreter_call";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the code interpreter.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// A local shell tool call item resource.
+///
+internal sealed record LocalShellToolCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for local shell call items.
+ ///
+ public const string ItemType = "local_shell_call";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the local shell call.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// A local shell tool call output item resource.
+///
+internal sealed record LocalShellToolCallOutputItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for local shell call output items.
+ ///
+ public const string ItemType = "local_shell_call_output";
+
+ ///
+ public override string Type => ItemType;
+
+ ///
+ /// The status of the local shell call output.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; init; }
+}
+
+///
+/// An MCP list tools item resource.
+///
+internal sealed record MCPListToolsItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for MCP list tools items.
+ ///
+ public const string ItemType = "mcp_list_tools";
+
+ ///
+ public override string Type => ItemType;
+}
+
+///
+/// An MCP approval request item resource.
+///
+internal sealed record MCPApprovalRequestItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for MCP approval request items.
+ ///
+ public const string ItemType = "mcp_approval_request";
+
+ ///
+ public override string Type => ItemType;
+}
+
+///
+/// An MCP approval response item resource.
+///
+internal sealed record MCPApprovalResponseItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for MCP approval response items.
+ ///
+ public const string ItemType = "mcp_approval_response";
+
+ ///
+ public override string Type => ItemType;
+}
+
+///
+/// An MCP call item resource.
+///
+internal sealed record MCPCallItemResource : ItemResource
+{
+ ///
+ /// The constant item type identifier for MCP call items.
+ ///
+ public const string ItemType = "mcp_call";
+
+ ///
+ public override string Type => ItemType;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/PromptReference.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/PromptReference.cs
new file mode 100644
index 0000000000..d14e94473c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/PromptReference.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Reference to a prompt template and its variables.
+///
+internal sealed record PromptReference
+{
+ ///
+ /// The ID of the prompt template to use.
+ ///
+ [JsonPropertyName("id")]
+ public required string Id { get; init; }
+
+ ///
+ /// Variables to substitute in the prompt template.
+ ///
+ [JsonPropertyName("variables")]
+ public Dictionary? Variables { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ReasoningOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ReasoningOptions.cs
new file mode 100644
index 0000000000..8be910278a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ReasoningOptions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Configuration options for reasoning models.
+///
+internal sealed record ReasoningOptions
+{
+ ///
+ /// Constrains effort on reasoning for reasoning models.
+ /// Currently supported values are "low", "medium", and "high".
+ /// Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning.
+ ///
+ [JsonPropertyName("effort")]
+ public string? Effort { get; init; }
+
+ ///
+ /// A summary of the reasoning performed by the model.
+ /// One of "concise" or "detailed".
+ ///
+ [JsonPropertyName("summary")]
+ public string? Summary { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs
new file mode 100644
index 0000000000..260aa2478c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs
@@ -0,0 +1,375 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// The status of a response generation.
+///
+[JsonConverter(typeof(SnakeCaseEnumConverter))]
+public enum ResponseStatus
+{
+ ///
+ /// The response has been completed.
+ ///
+ Completed,
+
+ ///
+ /// The response generation has failed.
+ ///
+ Failed,
+
+ ///
+ /// The response generation is in progress.
+ ///
+ InProgress,
+
+ ///
+ /// The response generation has been cancelled.
+ ///
+ Cancelled,
+
+ ///
+ /// The response is queued for processing.
+ ///
+ Queued,
+
+ ///
+ /// The response is incomplete.
+ ///
+ Incomplete
+}
+
+///
+/// Response from creating a model response.
+///
+internal sealed record Response
+{
+ ///
+ /// The unique identifier for the response.
+ ///
+ [JsonPropertyName("id")]
+ public required string Id { get; init; }
+
+ ///
+ /// The object type, always "response".
+ ///
+ [JsonPropertyName("object")]
+ [SuppressMessage("Naming", "CA1720:Identifiers should not match keywords", Justification = "Matches API specification")]
+ public string Object => "response";
+
+ ///
+ /// The Unix timestamp (in seconds) for when the response was created.
+ ///
+ [JsonPropertyName("created_at")]
+ public required long CreatedAt { get; init; }
+
+ ///
+ /// The model used to generate the response.
+ ///
+ [JsonPropertyName("model")]
+ public string? Model { get; init; }
+
+ ///
+ /// The status of the response generation.
+ ///
+ [JsonPropertyName("status")]
+ public required ResponseStatus Status { get; init; }
+
+ ///
+ /// The agent used for this response.
+ ///
+ [JsonPropertyName("agent")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public AgentId? Agent { get; init; }
+
+ ///
+ /// Gets a value indicating whether the response is in a terminal state (completed, failed, cancelled, or incomplete).
+ ///
+ [JsonIgnore]
+ public bool IsTerminal => this.Status is ResponseStatus.Completed or ResponseStatus.Failed or ResponseStatus.Cancelled or ResponseStatus.Incomplete;
+
+ ///
+ /// An error object returned when the model fails to generate a response.
+ ///
+ [JsonPropertyName("error")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public ResponseError? Error { get; init; }
+
+ ///
+ /// Details about why the response is incomplete.
+ ///
+ [JsonPropertyName("incomplete_details")]
+ public IncompleteDetails? IncompleteDetails { get; init; }
+
+ ///
+ /// The output items (messages) generated in the response.
+ ///
+ [JsonPropertyName("output")]
+ public required IList Output { get; init; }
+
+ ///
+ /// A system (or developer) message inserted into the model's context.
+ ///
+ [JsonPropertyName("instructions")]
+ public string? Instructions { get; init; }
+
+ ///
+ /// Usage statistics for the response.
+ ///
+ [JsonPropertyName("usage")]
+ public required ResponseUsage Usage { get; init; }
+
+ ///
+ /// Whether to allow the model to run tool calls in parallel.
+ ///
+ [JsonPropertyName("parallel_tool_calls")]
+ public bool ParallelToolCalls { get; init; } = true;
+
+ ///
+ /// An array of tools the model may call while generating a response.
+ ///
+ [JsonPropertyName("tools")]
+ public required IList Tools { get; init; }
+
+ ///
+ /// How the model should select which tool (or tools) to use when generating a response.
+ ///
+ [JsonPropertyName("tool_choice")]
+ public JsonElement? ToolChoice { get; init; }
+
+ ///
+ /// What sampling temperature to use, between 0 and 2.
+ ///
+ [JsonPropertyName("temperature")]
+ public double? Temperature { get; init; }
+
+ ///
+ /// An alternative to sampling with temperature, called nucleus sampling.
+ ///
+ [JsonPropertyName("top_p")]
+ public double? TopP { get; init; }
+
+ ///
+ /// Set of up to 16 key-value pairs that can be attached to a response.
+ ///
+ [JsonPropertyName("metadata")]
+ public Dictionary? Metadata { get; init; }
+
+ ///
+ /// The conversation associated with this response.
+ ///
+ [JsonPropertyName("conversation")]
+ public ConversationReference? Conversation { get; init; }
+
+ ///
+ /// An upper bound for the number of tokens that can be generated for a response,
+ /// including visible output tokens and reasoning tokens.
+ ///
+ [JsonPropertyName("max_output_tokens")]
+ public int? MaxOutputTokens { get; init; }
+
+ ///
+ /// The unique ID of the previous response to the model.
+ ///
+ [JsonPropertyName("previous_response_id")]
+ public string? PreviousResponseId { get; init; }
+
+ ///
+ /// Configuration options for reasoning models.
+ ///
+ [JsonPropertyName("reasoning")]
+ public ReasoningOptions? Reasoning { get; init; }
+
+ ///
+ /// Whether the generated model response is stored for later retrieval.
+ ///
+ [JsonPropertyName("store")]
+ public bool? Store { get; init; }
+
+ ///
+ /// Configuration options for a text response from the model. Can be plain text or structured JSON data.
+ ///
+ [JsonPropertyName("text")]
+ public TextConfiguration? Text { get; init; }
+
+ ///
+ /// The truncation strategy used for the model response.
+ ///
+ [JsonPropertyName("truncation")]
+ public string? Truncation { get; init; }
+
+ ///
+ /// A unique identifier representing the end-user.
+ ///
+ [JsonPropertyName("user")]
+ public string? User { get; init; }
+
+ ///
+ /// The service tier used for the response.
+ ///
+ [JsonPropertyName("service_tier")]
+ public string? ServiceTier { get; init; }
+
+ ///
+ /// Whether to run the model response in the background.
+ ///
+ [JsonPropertyName("background")]
+ public bool? Background { get; init; }
+
+ ///
+ /// The maximum number of total calls to built-in tools that can be processed in a response.
+ ///
+ [JsonPropertyName("max_tool_calls")]
+ public int? MaxToolCalls { get; init; }
+
+ ///
+ /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position.
+ ///
+ [JsonPropertyName("top_logprobs")]
+ public int? TopLogprobs { get; init; }
+
+ ///
+ /// A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies.
+ ///
+ [JsonPropertyName("safety_identifier")]
+ public string? SafetyIdentifier { get; init; }
+
+ ///
+ /// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates.
+ ///
+ [JsonPropertyName("prompt_cache_key")]
+ public string? PromptCacheKey { get; init; }
+
+ ///
+ /// Reference to a prompt template and its variables.
+ ///
+ [JsonPropertyName("prompt")]
+ public PromptReference? Prompt { get; init; }
+}
+
+///
+/// An error object returned when the model fails to generate a response.
+///
+internal sealed record ResponseError
+{
+ ///
+ /// The error code for the response.
+ ///
+ [JsonPropertyName("code")]
+ public required string Code { get; init; }
+
+ ///
+ /// A human-readable description of the error.
+ ///
+ [JsonPropertyName("message")]
+ public required string Message { get; init; }
+}
+
+///
+/// Details about why the response is incomplete.
+///
+internal sealed record IncompleteDetails
+{
+ ///
+ /// The reason why the response is incomplete. One of "max_output_tokens" or "content_filter".
+ ///
+ [JsonPropertyName("reason")]
+ public required string Reason { get; init; }
+}
+
+///
+/// Usage statistics for a response.
+///
+internal sealed record ResponseUsage
+{
+ public static ResponseUsage Zero { get; } = new()
+ {
+ InputTokens = 0,
+ InputTokensDetails = new InputTokensDetails { CachedTokens = 0 },
+ OutputTokens = 0,
+ OutputTokensDetails = new OutputTokensDetails { ReasoningTokens = 0 },
+ TotalTokens = 0
+ };
+
+ ///
+ /// Number of tokens in the input.
+ ///
+ [JsonPropertyName("input_tokens")]
+ public required int InputTokens { get; init; }
+
+ ///
+ /// A detailed breakdown of the input tokens.
+ ///
+ [JsonPropertyName("input_tokens_details")]
+ public required InputTokensDetails InputTokensDetails { get; init; }
+
+ ///
+ /// Number of tokens in the output.
+ ///
+ [JsonPropertyName("output_tokens")]
+ public required int OutputTokens { get; init; }
+
+ ///
+ /// A detailed breakdown of the output tokens.
+ ///
+ [JsonPropertyName("output_tokens_details")]
+ public required OutputTokensDetails OutputTokensDetails { get; init; }
+
+ ///
+ /// Total number of tokens used.
+ ///
+ [JsonPropertyName("total_tokens")]
+ public required int TotalTokens { get; init; }
+
+ ///
+ /// Adds two instances together.
+ ///
+ /// The first usage instance.
+ /// The second usage instance.
+ /// A new instance with the combined values.
+ public static ResponseUsage operator +(ResponseUsage left, ResponseUsage right) =>
+ new()
+ {
+ InputTokens = left.InputTokens + right.InputTokens,
+ InputTokensDetails = new InputTokensDetails
+ {
+ CachedTokens = left.InputTokensDetails.CachedTokens + right.InputTokensDetails.CachedTokens
+ },
+ OutputTokens = left.OutputTokens + right.OutputTokens,
+ OutputTokensDetails = new OutputTokensDetails
+ {
+ ReasoningTokens = left.OutputTokensDetails.ReasoningTokens + right.OutputTokensDetails.ReasoningTokens
+ },
+ TotalTokens = left.TotalTokens + right.TotalTokens
+ };
+}
+
+///
+/// A detailed breakdown of the input tokens.
+///
+internal sealed record InputTokensDetails
+{
+ ///
+ /// The number of tokens that were retrieved from the cache.
+ ///
+ [JsonPropertyName("cached_tokens")]
+ public required int CachedTokens { get; init; }
+}
+
+///
+/// A detailed breakdown of the output tokens.
+///
+internal sealed record OutputTokensDetails
+{
+ ///
+ /// The number of reasoning tokens.
+ ///
+ [JsonPropertyName("reasoning_tokens")]
+ public required int ReasoningTokens { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ResponseInput.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ResponseInput.cs
new file mode 100644
index 0000000000..840adf87b0
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/ResponseInput.cs
@@ -0,0 +1,201 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Represents the input to a response request, which can be either a simple string or a list of messages.
+///
+[JsonConverter(typeof(ResponseInputJsonConverter))]
+internal sealed class ResponseInput : IEquatable
+{
+ private ResponseInput(string text)
+ {
+ this.Text = text ?? throw new ArgumentNullException(nameof(text));
+ this.Messages = null;
+ }
+
+ private ResponseInput(IReadOnlyList messages)
+ {
+ this.Messages = messages ?? throw new ArgumentNullException(nameof(messages));
+ this.Text = null;
+ }
+
+ ///
+ /// Creates a ResponseInput from a text string.
+ ///
+ public static ResponseInput FromText(string text) => new(text);
+
+ ///
+ /// Creates a ResponseInput from a list of messages.
+ ///
+ public static ResponseInput FromMessages(IReadOnlyList messages) => new(messages);
+
+ ///
+ /// Creates a ResponseInput from a list of messages.
+ ///
+ public static ResponseInput FromMessages(params InputMessage[] messages) => new(messages);
+
+ ///
+ /// Implicit conversion from string to ResponseInput.
+ ///
+ public static implicit operator ResponseInput(string text) => FromText(text);
+
+ ///
+ /// Implicit conversion from InputMessage array to ResponseInput.
+ ///
+ public static implicit operator ResponseInput(InputMessage[] messages) => FromMessages(messages);
+
+ ///
+ /// Implicit conversion from List to ResponseInput.
+ ///
+ public static implicit operator ResponseInput(List messages) => FromMessages(messages);
+
+ ///
+ /// Gets whether this input is a text string.
+ ///
+ public bool IsText => this.Text is not null;
+
+ ///
+ /// Gets whether this input is a list of messages.
+ ///
+ public bool IsMessages => this.Messages is not null;
+
+ ///
+ /// Gets the text value, or null if this is not a text input.
+ ///
+ public string? Text { get; }
+
+ ///
+ /// Gets the messages value, or null if this is not a messages input.
+ ///
+ public IReadOnlyList? Messages { get; }
+
+ ///
+ /// Gets the input as a list of InputMessage objects.
+ ///
+ public IReadOnlyList GetInputMessages()
+ {
+ if (this.Text is not null)
+ {
+ return [new InputMessage
+ {
+ Role = ChatRole.User,
+ Content = this.Text
+ }];
+ }
+
+ return this.Messages ?? [];
+ }
+
+ ///
+ public bool Equals(ResponseInput? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ // Both text
+ if (this.Text is not null && other.Text is not null)
+ {
+ return this.Text == other.Text;
+ }
+
+ // Both messages
+ if (this.Messages is not null && other.Messages is not null)
+ {
+ return this.Messages.SequenceEqual(other.Messages);
+ }
+
+ // One is text, one is messages - not equal
+ return false;
+ }
+
+ ///
+ public override bool Equals(object? obj) => this.Equals(obj as ResponseInput);
+
+ ///
+ public override int GetHashCode()
+ {
+ if (this.Text is not null)
+ {
+ return this.Text.GetHashCode();
+ }
+
+ if (this.Messages is not null)
+ {
+ return this.Messages.Count > 0 ? this.Messages[0].GetHashCode() : 0;
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Equality operator.
+ ///
+ public static bool operator ==(ResponseInput? left, ResponseInput? right)
+ {
+ return Equals(left, right);
+ }
+
+ ///
+ /// Inequality operator.
+ ///
+ public static bool operator !=(ResponseInput? left, ResponseInput? right)
+ {
+ return !Equals(left, right);
+ }
+}
+
+///
+/// JSON converter for ResponseInput.
+///
+internal sealed class ResponseInputJsonConverter : JsonConverter
+{
+ public override ResponseInput? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // Check if it's a string
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var text = reader.GetString();
+ return text is not null ? ResponseInput.FromText(text) : null;
+ }
+
+ // Check if it's an array
+ if (reader.TokenType == JsonTokenType.StartArray)
+ {
+ var messages = JsonSerializer.Deserialize(ref reader, ResponsesJsonContext.Default.ListInputMessage);
+ return messages is not null ? ResponseInput.FromMessages(messages) : null;
+ }
+
+ throw new JsonException($"Unexpected token type for ResponseInput: {reader.TokenType}");
+ }
+
+ public override void Write(Utf8JsonWriter writer, ResponseInput value, JsonSerializerOptions options)
+ {
+ if (value.IsText)
+ {
+ writer.WriteStringValue(value.Text);
+ }
+ else if (value.IsMessages)
+ {
+ JsonSerializer.Serialize(writer, value.Messages!, ResponsesJsonContext.Default.IReadOnlyListInputMessage);
+ }
+ else
+ {
+ throw new JsonException("ResponseInput has no value");
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamOptions.cs
new file mode 100644
index 0000000000..3c9a484ddf
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamOptions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Options for streaming responses. Only set this when you set stream: true.
+///
+internal sealed record StreamOptions
+{
+ ///
+ /// If set, an additional chunk will be streamed before the data: [DONE] message.
+ /// The usage field on this chunk shows the token usage statistics for the entire request,
+ /// and the choices field will always be an empty array.
+ ///
+ [JsonPropertyName("include_usage")]
+ public bool? IncludeUsage { get; init; }
+
+ ///
+ /// When true, stream obfuscation will be enabled. Stream obfuscation adds random characters
+ /// to an obfuscation field on streaming delta events to normalize payload sizes as a mitigation
+ /// to certain side-channel attacks. These obfuscation fields are included by default, but add
+ /// a small amount of overhead to the data stream. You can set include_obfuscation to false to
+ /// optimize for bandwidth if you trust the network links between your application and the OpenAI API.
+ ///
+ [JsonPropertyName("include_obfuscation")]
+ public bool? IncludeObfuscation { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamingResponseEvent.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamingResponseEvent.cs
new file mode 100644
index 0000000000..82f80bfa15
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/StreamingResponseEvent.cs
@@ -0,0 +1,519 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Abstract base class for all streaming response events in the OpenAI Responses API.
+/// Provides common properties shared across all streaming event types.
+///
+[JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
+[JsonDerivedType(typeof(StreamingResponseCreated), StreamingResponseCreated.EventType)]
+[JsonDerivedType(typeof(StreamingResponseInProgress), StreamingResponseInProgress.EventType)]
+[JsonDerivedType(typeof(StreamingResponseCompleted), StreamingResponseCompleted.EventType)]
+[JsonDerivedType(typeof(StreamingResponseIncomplete), StreamingResponseIncomplete.EventType)]
+[JsonDerivedType(typeof(StreamingResponseFailed), StreamingResponseFailed.EventType)]
+[JsonDerivedType(typeof(StreamingOutputItemAdded), StreamingOutputItemAdded.EventType)]
+[JsonDerivedType(typeof(StreamingOutputItemDone), StreamingOutputItemDone.EventType)]
+[JsonDerivedType(typeof(StreamingContentPartAdded), StreamingContentPartAdded.EventType)]
+[JsonDerivedType(typeof(StreamingContentPartDone), StreamingContentPartDone.EventType)]
+[JsonDerivedType(typeof(StreamingOutputTextDelta), StreamingOutputTextDelta.EventType)]
+[JsonDerivedType(typeof(StreamingOutputTextDone), StreamingOutputTextDone.EventType)]
+[JsonDerivedType(typeof(StreamingFunctionCallArgumentsDelta), StreamingFunctionCallArgumentsDelta.EventType)]
+[JsonDerivedType(typeof(StreamingFunctionCallArgumentsDone), StreamingFunctionCallArgumentsDone.EventType)]
+[JsonDerivedType(typeof(StreamingReasoningSummaryTextDelta), StreamingReasoningSummaryTextDelta.EventType)]
+[JsonDerivedType(typeof(StreamingReasoningSummaryTextDone), StreamingReasoningSummaryTextDone.EventType)]
+internal abstract record StreamingResponseEvent
+{
+ ///
+ /// Gets the type identifier for the streaming response event.
+ /// This property is used to discriminate between different event types during serialization.
+ ///
+ [JsonIgnore]
+ public abstract string Type { get; }
+
+ ///
+ /// Gets the sequence number of this event in the streaming response.
+ /// Events are numbered sequentially starting from 1 to maintain ordering.
+ ///
+ [JsonPropertyName("sequence_number")]
+ public int SequenceNumber { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that a new response has been created and streaming has begun.
+/// This is typically the first event sent in a streaming response sequence.
+///
+internal sealed record StreamingResponseCreated : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for response created events.
+ ///
+ public const string EventType = "response.created";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the response object that was created.
+ /// This contains metadata about the response including ID, creation timestamp, and other properties.
+ ///
+ [JsonPropertyName("response")]
+ public required Response Response { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that the response is in progress.
+///
+internal sealed record StreamingResponseInProgress : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for response in progress events.
+ ///
+ public const string EventType = "response.in_progress";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the response object that is in progress.
+ ///
+ [JsonPropertyName("response")]
+ public required Response Response { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that the response has been completed.
+/// This is typically the last event sent in a streaming response sequence.
+///
+internal sealed record StreamingResponseCompleted : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for response completed events.
+ ///
+ public const string EventType = "response.completed";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the completed response object.
+ /// This contains the final state of the response including all generated content and metadata.
+ ///
+ [JsonPropertyName("response")]
+ public required Response Response { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that the response finished as incomplete.
+///
+internal sealed record StreamingResponseIncomplete : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for response incomplete events.
+ ///
+ public const string EventType = "response.incomplete";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the incomplete response object.
+ ///
+ [JsonPropertyName("response")]
+ public required Response Response { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that the response has failed.
+///
+internal sealed record StreamingResponseFailed : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for response failed events.
+ ///
+ public const string EventType = "response.failed";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the failed response object.
+ ///
+ [JsonPropertyName("response")]
+ public required Response Response { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that a new output item has been added to the response.
+/// This event is sent when the AI agent produces a new piece of content during streaming.
+///
+internal sealed record StreamingOutputItemAdded : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for output item added events.
+ ///
+ public const string EventType = "response.output_item.added";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the index of the output in the response where this item was added.
+ /// Multiple outputs can exist in a single response, and this identifies which one.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the output item that was added.
+ /// This contains the actual content or data produced by the AI agent.
+ ///
+ [JsonPropertyName("item")]
+ public required ItemResource Item { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that an output item has been completed.
+/// This event is sent when the AI agent finishes producing a particular piece of content.
+///
+internal sealed record StreamingOutputItemDone : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for output item done events.
+ ///
+ public const string EventType = "response.output_item.done";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the index of the output in the response where this item was completed.
+ /// This corresponds to the same output index from the associated .
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the completed output item.
+ /// This contains the final version of the content produced by the AI agent.
+ ///
+ [JsonPropertyName("item")]
+ public required ItemResource Item { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that a new content part has been added to an output item.
+///
+internal sealed record StreamingContentPartAdded : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for content part added events.
+ ///
+ public const string EventType = "response.content_part.added";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the content index.
+ ///
+ [JsonPropertyName("content_index")]
+ public int ContentIndex { get; init; }
+
+ ///
+ /// Gets or sets the content part that was added.
+ ///
+ [JsonPropertyName("part")]
+ public required ItemContent Part { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that a content part has been completed.
+///
+internal sealed record StreamingContentPartDone : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for content part done events.
+ ///
+ public const string EventType = "response.content_part.done";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the content index.
+ ///
+ [JsonPropertyName("content_index")]
+ public int ContentIndex { get; init; }
+
+ ///
+ /// Gets or sets the completed content part.
+ ///
+ [JsonPropertyName("part")]
+ public required ItemContent Part { get; init; }
+}
+
+///
+/// Represents a streaming response event containing a text delta (incremental text chunk).
+///
+internal sealed record StreamingOutputTextDelta : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for output text delta events.
+ ///
+ public const string EventType = "response.output_text.delta";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the content index.
+ ///
+ [JsonPropertyName("content_index")]
+ public int ContentIndex { get; init; }
+
+ ///
+ /// Gets or sets the text delta (incremental chunk of text).
+ ///
+ [JsonPropertyName("delta")]
+ public required string Delta { get; init; }
+
+ ///
+ /// Gets or sets the log probability information for the output tokens.
+ ///
+ [JsonPropertyName("logprobs")]
+ public IList Logprobs { get; init; } = [];
+}
+
+///
+/// Represents a streaming response event indicating that output text has been completed.
+///
+internal sealed record StreamingOutputTextDone : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for output text done events.
+ ///
+ public const string EventType = "response.output_text.done";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the content index.
+ ///
+ [JsonPropertyName("content_index")]
+ public int ContentIndex { get; init; }
+
+ ///
+ /// Gets or sets the complete text.
+ ///
+ [JsonPropertyName("text")]
+ public required string Text { get; init; }
+}
+
+///
+/// Represents a streaming response event containing a function call arguments delta.
+///
+internal sealed record StreamingFunctionCallArgumentsDelta : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for function call arguments delta events.
+ ///
+ public const string EventType = "response.function_call_arguments.delta";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the function arguments delta.
+ ///
+ [JsonPropertyName("delta")]
+ public required string Delta { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that function call arguments are complete.
+///
+internal sealed record StreamingFunctionCallArgumentsDone : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for function call arguments done events.
+ ///
+ public const string EventType = "response.function_call_arguments.done";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the complete function arguments.
+ ///
+ [JsonPropertyName("arguments")]
+ public required string Arguments { get; init; }
+}
+
+///
+/// Represents a streaming response event containing a reasoning summary text delta (incremental text chunk).
+///
+internal sealed record StreamingReasoningSummaryTextDelta : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for reasoning summary text delta events.
+ ///
+ public const string EventType = "response.reasoning_summary_text.delta";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID this summary text delta is associated with.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the index of the summary part within the reasoning summary.
+ ///
+ [JsonPropertyName("summary_index")]
+ public int SummaryIndex { get; init; }
+
+ ///
+ /// Gets or sets the text delta that was added to the summary.
+ ///
+ [JsonPropertyName("delta")]
+ public required string Delta { get; init; }
+}
+
+///
+/// Represents a streaming response event indicating that reasoning summary text has been completed.
+///
+internal sealed record StreamingReasoningSummaryTextDone : StreamingResponseEvent
+{
+ ///
+ /// The constant event type identifier for reasoning summary text done events.
+ ///
+ public const string EventType = "response.reasoning_summary_text.done";
+
+ ///
+ [JsonIgnore]
+ public override string Type => EventType;
+
+ ///
+ /// Gets or sets the item ID this summary text is associated with.
+ ///
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; init; }
+
+ ///
+ /// Gets or sets the output index.
+ ///
+ [JsonPropertyName("output_index")]
+ public int OutputIndex { get; init; }
+
+ ///
+ /// Gets or sets the index of the summary part within the reasoning summary.
+ ///
+ [JsonPropertyName("summary_index")]
+ public int SummaryIndex { get; init; }
+
+ ///
+ /// Gets or sets the full text of the completed reasoning summary.
+ ///
+ [JsonPropertyName("text")]
+ public required string Text { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/TextConfiguration.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/TextConfiguration.cs
new file mode 100644
index 0000000000..dc590030e6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/TextConfiguration.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+///
+/// Configuration options for a text response from the model.
+///
+internal sealed record TextConfiguration
+{
+ ///
+ /// The format configuration for the text response.
+ /// Can specify plain text, JSON object, or JSON schema for structured outputs.
+ ///
+ [JsonPropertyName("format")]
+ public ResponseTextFormatConfiguration? Format { get; init; }
+
+ ///
+ /// Constrains the verbosity of the model's response.
+ /// Lower values will result in more concise responses, while higher values will result in more verbose responses.
+ /// Supported values are "low", "medium", and "high". Defaults to "medium".
+ ///
+ [JsonPropertyName("verbosity")]
+ public string? Verbosity { get; init; }
+}
+
+///
+/// Base class for response text format configurations.
+/// This is a discriminated union based on the "type" property.
+///
+[JsonPolymorphic(TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
+[JsonDerivedType(typeof(ResponseTextFormatConfigurationText), "text")]
+[JsonDerivedType(typeof(ResponseTextFormatConfigurationJsonObject), "json_object")]
+[JsonDerivedType(typeof(ResponseTextFormatConfigurationJsonSchema), "json_schema")]
+internal abstract record ResponseTextFormatConfiguration
+{
+ ///
+ /// The type of response format.
+ ///
+ [JsonIgnore]
+ public abstract string Type { get; }
+}
+
+///
+/// Plain text response format configuration.
+///
+internal sealed record ResponseTextFormatConfigurationText : ResponseTextFormatConfiguration
+{
+ ///
+ /// Gets the type of response format. Always "text".
+ ///
+ [JsonIgnore]
+ public override string Type => "text";
+}
+
+///
+/// JSON object response format configuration.
+/// Ensures the message the model generates is valid JSON.
+///
+internal sealed record ResponseTextFormatConfigurationJsonObject : ResponseTextFormatConfiguration
+{
+ ///
+ /// Gets the type of response format. Always "json_object".
+ ///
+ [JsonIgnore]
+ public override string Type => "json_object";
+}
+
+///
+/// JSON schema response format configuration with structured output schema.
+///
+internal sealed record ResponseTextFormatConfigurationJsonSchema : ResponseTextFormatConfiguration
+{
+ ///
+ /// Gets the type of response format. Always "json_schema".
+ ///
+ [JsonIgnore]
+ public override string Type => "json_schema";
+
+ ///
+ /// The name of the response format. Must be a-z, A-Z, 0-9, or contain
+ /// underscores and dashes, with a maximum length of 64.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; init; }
+
+ ///
+ /// A description of what the response format is for, used by the model to
+ /// determine how to respond in the format.
+ ///
+ [JsonPropertyName("description")]
+ public string? Description { get; init; }
+
+ ///
+ /// The JSON schema for structured outputs.
+ ///
+ [JsonPropertyName("schema")]
+ public required Dictionary Schema { get; init; }
+
+ ///
+ /// Whether to enable strict schema adherence when generating the output.
+ /// If set to true, the model will always follow the exact schema defined in the schema field.
+ ///
+ [JsonPropertyName("strict")]
+ public bool? Strict { get; init; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/OpenAIResponsesRunOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/OpenAIResponsesRunOptions.cs
deleted file mode 100644
index 14ec11fe8e..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/OpenAIResponsesRunOptions.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
-
-internal sealed class OpenAIResponsesRunOptions : AgentRunOptions
-{
- public bool Background { get; init; }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonContext.cs
new file mode 100644
index 0000000000..0b37929b48
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonContext.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
+
+[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ AllowOutOfOrderMetadataProperties = true,
+ WriteIndented = false)]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(CreateResponse))]
+[JsonSerializable(typeof(Response))]
+[JsonSerializable(typeof(StreamingResponseEvent))]
+[JsonSerializable(typeof(StreamingResponseCreated))]
+[JsonSerializable(typeof(StreamingResponseInProgress))]
+[JsonSerializable(typeof(StreamingResponseCompleted))]
+[JsonSerializable(typeof(StreamingResponseIncomplete))]
+[JsonSerializable(typeof(StreamingResponseFailed))]
+[JsonSerializable(typeof(StreamingOutputItemAdded))]
+[JsonSerializable(typeof(StreamingOutputItemDone))]
+[JsonSerializable(typeof(StreamingContentPartAdded))]
+[JsonSerializable(typeof(StreamingContentPartDone))]
+[JsonSerializable(typeof(StreamingOutputTextDelta))]
+[JsonSerializable(typeof(StreamingOutputTextDone))]
+[JsonSerializable(typeof(StreamingFunctionCallArgumentsDelta))]
+[JsonSerializable(typeof(StreamingFunctionCallArgumentsDone))]
+[JsonSerializable(typeof(ReasoningOptions))]
+[JsonSerializable(typeof(ResponseUsage))]
+[JsonSerializable(typeof(ResponseError))]
+[JsonSerializable(typeof(IncompleteDetails))]
+[JsonSerializable(typeof(InputTokensDetails))]
+[JsonSerializable(typeof(OutputTokensDetails))]
+[JsonSerializable(typeof(ConversationReference))]
+[JsonSerializable(typeof(ResponseInput))]
+[JsonSerializable(typeof(InputMessage))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(IReadOnlyList))]
+[JsonSerializable(typeof(InputMessageContent))]
+[JsonSerializable(typeof(ResponseStatus))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(IList))]
+[JsonSerializable(typeof(ItemResource))]
+[JsonSerializable(typeof(ResponsesMessageItemResource))]
+[JsonSerializable(typeof(ResponsesAssistantMessageItemResource))]
+[JsonSerializable(typeof(ResponsesUserMessageItemResource))]
+[JsonSerializable(typeof(ResponsesSystemMessageItemResource))]
+[JsonSerializable(typeof(ResponsesDeveloperMessageItemResource))]
+[JsonSerializable(typeof(FileSearchToolCallItemResource))]
+[JsonSerializable(typeof(FunctionToolCallItemResource))]
+[JsonSerializable(typeof(FunctionToolCallOutputItemResource))]
+[JsonSerializable(typeof(ComputerToolCallItemResource))]
+[JsonSerializable(typeof(ComputerToolCallOutputItemResource))]
+[JsonSerializable(typeof(WebSearchToolCallItemResource))]
+[JsonSerializable(typeof(ReasoningItemResource))]
+[JsonSerializable(typeof(ItemReferenceItemResource))]
+[JsonSerializable(typeof(ImageGenerationToolCallItemResource))]
+[JsonSerializable(typeof(CodeInterpreterToolCallItemResource))]
+[JsonSerializable(typeof(LocalShellToolCallItemResource))]
+[JsonSerializable(typeof(LocalShellToolCallOutputItemResource))]
+[JsonSerializable(typeof(MCPListToolsItemResource))]
+[JsonSerializable(typeof(MCPApprovalRequestItemResource))]
+[JsonSerializable(typeof(MCPApprovalResponseItemResource))]
+[JsonSerializable(typeof(MCPCallItemResource))]
+[JsonSerializable(typeof(IList))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(ItemContent))]
+[JsonSerializable(typeof(ItemContentInputText))]
+[JsonSerializable(typeof(ItemContentInputAudio))]
+[JsonSerializable(typeof(ItemContentInputImage))]
+[JsonSerializable(typeof(ItemContentInputFile))]
+[JsonSerializable(typeof(ItemContentOutputText))]
+[JsonSerializable(typeof(ItemContentOutputAudio))]
+[JsonSerializable(typeof(ItemContentRefusal))]
+[JsonSerializable(typeof(TextConfiguration))]
+[JsonSerializable(typeof(ResponseTextFormatConfiguration))]
+[JsonSerializable(typeof(ResponseTextFormatConfigurationText))]
+[JsonSerializable(typeof(ResponseTextFormatConfigurationJsonObject))]
+[JsonSerializable(typeof(ResponseTextFormatConfigurationJsonSchema))]
+[ExcludeFromCodeCoverage]
+internal sealed partial class ResponsesJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonSerializerOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonSerializerOptions.cs
new file mode 100644
index 0000000000..5f014466fc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/ResponsesJsonSerializerOptions.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
+
+///
+/// Extension methods for JSON serialization.
+///
+internal static class ResponsesJsonSerializerOptions
+{
+ ///
+ /// Gets the default JSON serializer options.
+ ///
+ public static JsonSerializerOptions Default { get; } = Create();
+
+ private static JsonSerializerOptions Create()
+ {
+ JsonSerializerOptions options = new(ResponsesJsonContext.Default.Options);
+ options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
+ options.MakeReadOnly();
+ return options;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AssistantMessageEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AssistantMessageEventGenerator.cs
new file mode 100644
index 0000000000..21c4c0c8c1
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AssistantMessageEventGenerator.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A state machine for generating streaming events from assistant message content.
+/// Processes AIContent instances one at a time and emits appropriate streaming events based on internal state.
+///
+internal sealed class AssistantMessageEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private State _currentState = State.Initial;
+ private readonly string _itemId = idGenerator.GenerateMessageId();
+ private readonly StringBuilder _text = new();
+
+ ///
+ /// Represents the state of the event generator.
+ ///
+ private enum State
+ {
+ Initial,
+ AccumulatingText,
+ Completed
+ }
+
+ public override bool IsSupported(AIContent content) => content is TextContent;
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._currentState == State.Completed)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ // Only process TextContent
+ if (content is not TextContent textContent)
+ {
+ yield break;
+ }
+
+ // If is the first content, emit initial events
+ if (this._currentState == State.Initial)
+ {
+ var incompleteItem = new ResponsesAssistantMessageItemResource
+ {
+ Id = this._itemId,
+ Status = ResponsesMessageItemResourceStatus.InProgress,
+ Content = []
+ };
+
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = incompleteItem
+ };
+
+ yield return new StreamingContentPartAdded
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = this._itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = new ItemContentOutputText { Text = string.Empty, Annotations = [], Logprobs = [] }
+ };
+
+ this._currentState = State.AccumulatingText;
+ }
+
+ // Accumulate text and emit delta event
+ this._text.Append(textContent.Text);
+
+ yield return new StreamingOutputTextDelta
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = this._itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Delta = textContent.Text
+ };
+ }
+
+ public override IEnumerable Complete()
+ {
+ if (this._currentState == State.Completed)
+ {
+ throw new InvalidOperationException("Complete has already been called.");
+ }
+
+ // If no content was processed, emit initial events first
+ if (this._currentState == State.Initial)
+ {
+ yield break;
+ }
+
+ // Emit final events
+ var finalText = this._text.ToString();
+ var itemContent = new ItemContentOutputText
+ {
+ Text = finalText,
+ Annotations = [],
+ Logprobs = []
+ };
+
+ // Emit response.output_text.done event
+ yield return new StreamingOutputTextDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = this._itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Text = finalText
+ };
+
+ yield return new StreamingContentPartDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = this._itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = new ResponsesAssistantMessageItemResource
+ {
+ Id = this._itemId,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = [itemContent]
+ }
+ };
+
+ this._currentState = State.Completed;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AudioContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AudioContentEventGenerator.cs
new file mode 100644
index 0000000000..0d80f93154
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AudioContentEventGenerator.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A generator for streaming events from audio content.
+///
+internal sealed class AudioContentEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private bool _isCompleted;
+
+ public override bool IsSupported(AIContent content) =>
+ content is DataContent dataContent && dataContent.HasTopLevelMediaType("audio");
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._isCompleted)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ if (content is not DataContent audioData || !audioData.HasTopLevelMediaType("audio"))
+ {
+ throw new InvalidOperationException("AudioContentEventGenerator only supports audio DataContent.");
+ }
+
+ var itemId = idGenerator.GenerateMessageId();
+ var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputAudio;
+
+ if (itemContent == null)
+ {
+ throw new InvalidOperationException("Failed to convert audio content to ItemContentInputAudio.");
+ }
+
+ var item = new ResponsesAssistantMessageItemResource
+ {
+ Id = itemId,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = [itemContent]
+ };
+
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ yield return new StreamingContentPartAdded
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingContentPartDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ this._isCompleted = true;
+ }
+
+ public override IEnumerable Complete()
+ {
+ this._isCompleted = true;
+ return [];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ErrorContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ErrorContentEventGenerator.cs
new file mode 100644
index 0000000000..8320b1f977
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ErrorContentEventGenerator.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A generator for streaming events from error content.
+///
+internal sealed class ErrorContentEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private bool _isCompleted;
+
+ public override bool IsSupported(AIContent content) => content is ErrorContent;
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._isCompleted)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ if (content is not ErrorContent)
+ {
+ throw new InvalidOperationException("ErrorContentEventGenerator only supports ErrorContent.");
+ }
+
+ var itemId = idGenerator.GenerateMessageId();
+ var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentRefusal;
+
+ if (itemContent == null)
+ {
+ throw new InvalidOperationException("Failed to convert error content to ItemContentRefusal.");
+ }
+
+ var item = new ResponsesAssistantMessageItemResource
+ {
+ Id = itemId,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = [itemContent]
+ };
+
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ yield return new StreamingContentPartAdded
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingContentPartDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ this._isCompleted = true;
+ }
+
+ public override IEnumerable Complete()
+ {
+ this._isCompleted = true;
+ return [];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FileContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FileContentEventGenerator.cs
new file mode 100644
index 0000000000..bf60c2f1cf
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FileContentEventGenerator.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A generator for streaming events from file content (non-image, non-audio DataContent).
+///
+internal sealed class FileContentEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private bool _isCompleted;
+
+ public override bool IsSupported(AIContent content) =>
+ content is DataContent dataContent &&
+ !dataContent.HasTopLevelMediaType("image") &&
+ !dataContent.HasTopLevelMediaType("audio");
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._isCompleted)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ if (content is not DataContent fileData ||
+ fileData.HasTopLevelMediaType("image") ||
+ fileData.HasTopLevelMediaType("audio"))
+ {
+ throw new InvalidOperationException("FileContentEventGenerator only supports non-image, non-audio DataContent.");
+ }
+
+ var itemId = idGenerator.GenerateMessageId();
+ var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputFile;
+
+ if (itemContent == null)
+ {
+ throw new InvalidOperationException("Failed to convert file content to ItemContentInputFile.");
+ }
+
+ var item = new ResponsesAssistantMessageItemResource
+ {
+ Id = itemId,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = [itemContent]
+ };
+
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ yield return new StreamingContentPartAdded
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingContentPartDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ this._isCompleted = true;
+ }
+
+ public override IEnumerable Complete()
+ {
+ this._isCompleted = true;
+ return [];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionCallEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionCallEventGenerator.cs
new file mode 100644
index 0000000000..74f5ffe4ae
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionCallEventGenerator.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A generator for streaming events from function call content.
+///
+internal sealed class FunctionCallEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex,
+ JsonSerializerOptions jsonSerializerOptions) : StreamingEventGenerator
+{
+ private bool _isCompleted;
+
+ public override bool IsSupported(AIContent content) => content is FunctionCallContent;
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._isCompleted)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ if (content is not FunctionCallContent functionCallContent)
+ {
+ throw new InvalidOperationException("FunctionCallEventGenerator only supports FunctionCallContent.");
+ }
+
+ var item = functionCallContent.ToFunctionToolCallItemResource(idGenerator.GenerateFunctionCallId(), jsonSerializerOptions);
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ yield return new StreamingFunctionCallArgumentsDelta
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = item.Id,
+ OutputIndex = outputIndex,
+ Delta = item.Arguments
+ };
+
+ yield return new StreamingFunctionCallArgumentsDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = item.Id,
+ OutputIndex = outputIndex,
+ Arguments = item.Arguments
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ this._isCompleted = true;
+ }
+
+ public override IEnumerable Complete()
+ {
+ this._isCompleted = true;
+ return [];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionResultEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionResultEventGenerator.cs
new file mode 100644
index 0000000000..1c7810a825
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/FunctionResultEventGenerator.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A generator for streaming events from function result content.
+///
+internal sealed class FunctionResultEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private bool _isCompleted;
+
+ public override bool IsSupported(AIContent content) => content is FunctionResultContent;
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._isCompleted)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ if (content is not FunctionResultContent functionResultContent)
+ {
+ throw new InvalidOperationException("FunctionResultEventGenerator only supports FunctionResultContent.");
+ }
+
+ var item = functionResultContent.ToFunctionToolCallOutputItemResource(idGenerator.GenerateFunctionOutputId());
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ this._isCompleted = true;
+ }
+
+ public override IEnumerable Complete()
+ {
+ this._isCompleted = true;
+ return [];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/HostedFileContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/HostedFileContentEventGenerator.cs
new file mode 100644
index 0000000000..5846858aa2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/HostedFileContentEventGenerator.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A generator for streaming events from hosted file content.
+///
+internal sealed class HostedFileContentEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private bool _isCompleted;
+
+ public override bool IsSupported(AIContent content) => content is HostedFileContent;
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._isCompleted)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ if (content is not HostedFileContent)
+ {
+ throw new InvalidOperationException("HostedFileContentEventGenerator only supports HostedFileContent.");
+ }
+
+ var itemId = idGenerator.GenerateMessageId();
+ var itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputFile;
+
+ if (itemContent == null)
+ {
+ throw new InvalidOperationException("Failed to convert hosted file content to ItemContentInputFile.");
+ }
+
+ var item = new ResponsesAssistantMessageItemResource
+ {
+ Id = itemId,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = [itemContent]
+ };
+
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ yield return new StreamingContentPartAdded
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingContentPartDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ this._isCompleted = true;
+ }
+
+ public override IEnumerable Complete()
+ {
+ this._isCompleted = true;
+ return [];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ImageContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ImageContentEventGenerator.cs
new file mode 100644
index 0000000000..0b80abbfd2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/ImageContentEventGenerator.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A generator for streaming events from image content.
+///
+internal sealed class ImageContentEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private bool _isCompleted;
+
+ public override bool IsSupported(AIContent content) =>
+ content is UriContent uriContent && uriContent.HasTopLevelMediaType("image") ||
+ content is DataContent dataContent && dataContent.HasTopLevelMediaType("image");
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._isCompleted)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ ItemContentInputImage? itemContent = ItemContentConverter.ToItemContent(content) as ItemContentInputImage;
+
+ if (itemContent == null)
+ {
+ throw new InvalidOperationException("ImageContentEventGenerator only supports image UriContent and DataContent.");
+ }
+
+ var itemId = idGenerator.GenerateMessageId();
+
+ var item = new ResponsesAssistantMessageItemResource
+ {
+ Id = itemId,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = [itemContent]
+ };
+
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ yield return new StreamingContentPartAdded
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingContentPartDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = itemId,
+ OutputIndex = outputIndex,
+ ContentIndex = 0,
+ Part = itemContent
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = item
+ };
+
+ this._isCompleted = true;
+ }
+
+ public override IEnumerable Complete()
+ {
+ this._isCompleted = true;
+ return [];
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/SequenceNumber.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/SequenceNumber.cs
new file mode 100644
index 0000000000..d119275f71
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/SequenceNumber.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// Implements a sequence number generator.
+///
+internal sealed class SequenceNumber
+{
+ private int _sequenceNumber;
+
+ ///
+ /// Gets the next sequence number.
+ ///
+ /// The next sequence number.
+ public int Increment() => this._sequenceNumber++;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/StreamingEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/StreamingEventGenerator.cs
new file mode 100644
index 0000000000..ab1a68b6af
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/StreamingEventGenerator.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// Abstract base class for generating streaming events from instances
+///
+internal abstract class StreamingEventGenerator
+{
+ ///
+ /// Determines if the provided content is supported by this generator.
+ ///
+ /// The to check.
+ /// True if the content is supported, false otherwise.
+ public abstract bool IsSupported(AIContent content);
+
+ ///
+ /// Processes a single instance and yields streaming events based on the current state.
+ ///
+ /// The to process.
+ /// An enumerable of streaming events generated from processing the content.
+ public abstract IEnumerable ProcessContent(AIContent content);
+
+ ///
+ /// Completes the event generation and emits final events.
+ ///
+ /// An enumerable of final streaming events.
+ public abstract IEnumerable Complete();
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs
new file mode 100644
index 0000000000..5be8d73aa9
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/TextReasoningContentEventGenerator.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
+
+///
+/// A state machine for generating streaming events from reasoning text content.
+/// Processes TextReasoningContent instances one at a time and emits appropriate streaming events based on internal state.
+///
+internal sealed class TextReasoningContentEventGenerator(
+ IdGenerator idGenerator,
+ SequenceNumber seq,
+ int outputIndex) : StreamingEventGenerator
+{
+ private State _currentState = State.Initial;
+ private readonly string _itemId = idGenerator.GenerateMessageId();
+ private readonly StringBuilder _text = new();
+ private const int SummaryIndex = 0; // Summary index for reasoning summary text
+
+ ///
+ /// Represents the state of the event generator.
+ ///
+ private enum State
+ {
+ Initial,
+ AccumulatingText,
+ Completed
+ }
+
+ public override bool IsSupported(AIContent content) => content is TextReasoningContent;
+
+ public override IEnumerable ProcessContent(AIContent content)
+ {
+ if (this._currentState == State.Completed)
+ {
+ throw new InvalidOperationException("Cannot process content after the generator has been completed.");
+ }
+
+ // Only process TextReasoningContent
+ if (content is not TextReasoningContent reasoningContent)
+ {
+ yield break;
+ }
+
+ // If is the first content, emit initial events
+ if (this._currentState == State.Initial)
+ {
+ var incompleteItem = new ReasoningItemResource
+ {
+ Id = this._itemId,
+ Status = "in_progress"
+ };
+
+ yield return new StreamingOutputItemAdded
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = incompleteItem
+ };
+
+ this._currentState = State.AccumulatingText;
+ }
+
+ // Accumulate text and emit delta event
+ this._text.Append(reasoningContent.Text);
+
+ yield return new StreamingReasoningSummaryTextDelta
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = this._itemId,
+ OutputIndex = outputIndex,
+ SummaryIndex = SummaryIndex,
+ Delta = reasoningContent.Text
+ };
+ }
+
+ public override IEnumerable Complete()
+ {
+ if (this._currentState == State.Completed)
+ {
+ throw new InvalidOperationException("Complete has already been called.");
+ }
+
+ // If no content was processed, emit initial events first
+ if (this._currentState == State.Initial)
+ {
+ yield break;
+ }
+
+ // Emit final events
+ var finalText = this._text.ToString();
+
+ yield return new StreamingReasoningSummaryTextDone
+ {
+ SequenceNumber = seq.Increment(),
+ ItemId = this._itemId,
+ OutputIndex = outputIndex,
+ SummaryIndex = SummaryIndex,
+ Text = finalText
+ };
+
+ yield return new StreamingOutputItemDone
+ {
+ SequenceNumber = seq.Increment(),
+ OutputIndex = outputIndex,
+ Item = new ReasoningItemResource
+ {
+ Id = this._itemId,
+ Status = "completed"
+ }
+ };
+
+ this._currentState = State.Completed;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/AgentRunResponseUpdateExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/AgentRunResponseUpdateExtensions.cs
deleted file mode 100644
index 63e66f25cb..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/AgentRunResponseUpdateExtensions.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Extensions.AI;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
-
-internal static class AgentRunResponseUpdateExtensions
-{
- ///
- /// Converts an instance to a .
- ///
- /// The to convert. Cannot be null.
- /// The role of agent run response contents. By default is .
- /// A populated with values from .
- public static ChatResponse AsChatResponse(this AgentRunResponseUpdate response, ChatRole? role = null) => new()
- {
- CreatedAt = response.CreatedAt,
- ResponseId = response.ResponseId,
- RawRepresentation = response.RawRepresentation,
- AdditionalProperties = response.AdditionalProperties,
- Messages = [new ChatMessage(response.Role ?? role ?? ChatRole.Assistant, response.Contents)]
- };
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/OpenAIResponseJsonConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/OpenAIResponseJsonConverter.cs
deleted file mode 100644
index c2ff8131d8..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/OpenAIResponseJsonConverter.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.ClientModel.Primitives;
-using System.Diagnostics;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using OpenAI.Responses;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
-
-internal sealed class OpenAIResponseJsonConverter : JsonConverter
-{
- public override OpenAIResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var item = OpenAIResponsesModelFactory.OpenAIResponse();
- var jsonModel = item as IJsonModel;
- Debug.Assert(jsonModel is not null, "OpenAIResponse should implement IJsonModel");
-
- return jsonModel.Create(ref reader, ModelReaderWriterOptions.Json);
- }
-
- public override void Write(Utf8JsonWriter writer, OpenAIResponse value, JsonSerializerOptions options)
- {
- var jsonModel = value as IJsonModel;
- Debug.Assert(jsonModel is not null, "OpenAIResponse should implement IJsonModel");
-
- jsonModel.Write(writer, ModelReaderWriterOptions.Json);
- }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/OpenAIResponsesJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/OpenAIResponsesJsonUtilities.cs
deleted file mode 100644
index d7ee539526..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/OpenAIResponsesJsonUtilities.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Model;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
-
-internal static partial class OpenAIResponsesJsonUtilities
-{
- ///
- /// Gets the singleton used as the default in JSON serialization operations.
- ///
- ///
- ///
- /// For Native AOT or applications disabling , this instance
- /// includes source generated contracts for all common exchange types contained in this library.
- ///
- ///
- /// It additionally turns on the following settings:
- ///
- /// - Enables defaults.
- /// - Enables as the default ignore condition for properties.
- /// - Enables as the default number handling for number types.
- ///
- ///
- ///
- public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
-
- ///
- /// Creates default options to use for agents-related serialization.
- ///
- /// The configured options.
- [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
- [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
- private static JsonSerializerOptions CreateDefaultOptions()
- {
- JsonSerializerOptions options = new(JsonContext.Default.Options);
-
- options.Converters.Add(new ResponseItemJsonConverter());
- options.Converters.Add(new OpenAIResponseJsonConverter());
-
- options.MakeReadOnly();
- return options;
- }
-
- [JsonSerializable(typeof(StreamingResponseEventBase))]
-
- [ExcludeFromCodeCoverage]
- private sealed partial class JsonContext : JsonSerializerContext;
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseCreationOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseCreationOptionsExtensions.cs
deleted file mode 100644
index a91811c793..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseCreationOptionsExtensions.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.Reflection;
-using Microsoft.Shared.Diagnostics;
-using OpenAI.Responses;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
-
-[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "Specifically for accessing hidden members")]
-[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Specifically for accessing hidden members")]
-internal static class ResponseCreationOptionsExtensions
-{
- private static readonly Func _getStreamNullable;
- private static readonly Func> _getInput;
-
- static ResponseCreationOptionsExtensions()
- {
- // OpenAI SDK does not have a simple way to get the input as a c# object.
- // However, it does parse most of the interesting fields into internal properties of `ResponseCreationOptions` object.
-
- // --- Stream (internal bool? Stream { get; set; }) ---
- const string streamPropName = "Stream";
- var streamProp = typeof(ResponseCreationOptions).GetProperty(streamPropName, BindingFlags.Instance | BindingFlags.NonPublic)
- ?? throw new MissingMemberException(typeof(ResponseCreationOptions).FullName!, streamPropName);
- var streamGetter = streamProp.GetGetMethod(nonPublic: true) ?? throw new MissingMethodException($"{streamPropName} getter not found.");
-
- _getStreamNullable = streamGetter.CreateDelegate>();
-
- // --- Input (internal IList Input { get; set; }) ---
- const string inputPropName = "Input";
- var inputProp = typeof(ResponseCreationOptions).GetProperty(inputPropName, BindingFlags.Instance | BindingFlags.NonPublic)
- ?? throw new MissingMemberException(typeof(ResponseCreationOptions).FullName!, inputPropName);
- var inputGetter = inputProp.GetGetMethod(nonPublic: true)
- ?? throw new MissingMethodException($"{inputPropName} getter not found.");
-
- _getInput = inputGetter.CreateDelegate>>();
- }
-
- public static bool GetStream(this ResponseCreationOptions options)
- {
- Throw.IfNull(options);
- return _getStreamNullable(options) ?? false;
- }
-
- public static IList GetInput(this ResponseCreationOptions options)
- {
- Throw.IfNull(options);
- return _getInput(options);
- }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseItemExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseItemExtensions.cs
deleted file mode 100644
index f3e6dbb98d..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseItemExtensions.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Reflection;
-using Microsoft.Shared.Diagnostics;
-using OpenAI.Responses;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
-
-[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "Specifically for accessing hidden members")]
-[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Specifically for accessing hidden members")]
-internal static class ResponseItemExtensions
-{
- private static readonly Action _setId;
-
- static ResponseItemExtensions()
- {
- // OpenAI SDK ResponseItem has an internal setter for Id property.
- // We need to access it via reflection to set the Id when creating response items.
-
- // --- Id (public string Id { get; internal set; }) ---
- const string idPropName = "Id";
- var idProp = typeof(ResponseItem).GetProperty(idPropName, BindingFlags.Instance | BindingFlags.Public)
- ?? throw new MissingMemberException(typeof(ResponseItem).FullName!, idPropName);
- var idSetter = idProp.GetSetMethod(nonPublic: true) ?? throw new MissingMethodException($"{idPropName} setter not found.");
-
- _setId = idSetter.CreateDelegate>();
- }
-
- ///
- /// Sets the Id property on a ResponseItem using reflection to access the internal setter.
- ///
- /// The ResponseItem to set the Id on.
- /// The Id value to set.
- public static void SetId(this ResponseItem responseItem, string id)
- {
- Throw.IfNull(responseItem);
- _setId(responseItem, id);
- }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseItemJsonConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseItemJsonConverter.cs
deleted file mode 100644
index 6b39029b84..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Utils/ResponseItemJsonConverter.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.ClientModel.Primitives;
-using System.Diagnostics;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using OpenAI.Responses;
-
-namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Utils;
-
-internal sealed class ResponseItemJsonConverter : JsonConverter
-{
- public override ResponseItem? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var item = ResponseItem.CreateUserMessageItem(""); // no other way to instantiate it.
- var jsonModel = item as IJsonModel;
- Debug.Assert(jsonModel is not null, "ResponseItem should implement IJsonModel");
- return jsonModel.Create(ref reader, ModelReaderWriterOptions.Json);
- }
-
- public override void Write(Utf8JsonWriter writer, ResponseItem? value, JsonSerializerOptions options)
- {
- if (value is null)
- {
- writer.WriteNullValue();
- return;
- }
-
- var jsonmodel = value as IJsonModel;
- Debug.Assert(jsonmodel is not null, "ResponseItem should implement IJsonModel");
- jsonmodel.Write(writer, ModelReaderWriterOptions.Json);
- }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..7fab6586a9
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ServiceCollectionExtensions.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses;
+using Microsoft.AspNetCore.Http.Json;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods for to configure OpenAI Responses support.
+///
+public static class MicrosoftAgentAIHostingOpenAIServiceCollectionExtensions
+{
+ ///
+ /// Adds support for exposing instances via OpenAI Responses.
+ ///
+ /// The to configure.
+ /// The for method chaining.
+ public static IServiceCollection AddOpenAIResponses(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ services.Configure(options => options.SerializerOptions.TypeInfoResolverChain.Add(ResponsesJsonSerializerOptions.Default.TypeInfoResolver!));
+
+ return services;
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTestBase.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTestBase.cs
new file mode 100644
index 0000000000..eb6de34fff
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTestBase.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
+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.Tests;
+
+///
+/// Base class for conformance tests that load request/response traces from disk.
+///
+public abstract class ConformanceTestBase : IAsyncDisposable
+{
+ protected const string TracesBasePath = "ConformanceTraces/Responses";
+ private WebApplication? _app;
+ private HttpClient? _httpClient;
+
+ ///
+ /// Loads a JSON file from the conformance traces directory.
+ ///
+ protected static string LoadTraceFile(string relativePath)
+ {
+ var fullPath = Path.Combine(TracesBasePath, relativePath);
+
+ if (!File.Exists(fullPath))
+ {
+ throw new FileNotFoundException($"Conformance trace file not found: {fullPath}");
+ }
+
+ return File.ReadAllText(fullPath);
+ }
+
+ ///
+ /// Loads a JSON document from the conformance traces directory.
+ ///
+ protected static JsonDocument LoadTraceDocument(string relativePath)
+ {
+ var json = LoadTraceFile(relativePath);
+ return JsonDocument.Parse(json);
+ }
+
+ ///
+ /// Asserts that a JSON element exists (property is present, value can be null).
+ ///
+ protected static void AssertJsonPropertyExists(JsonElement element, string propertyName)
+ {
+ if (!element.TryGetProperty(propertyName, out _))
+ {
+ throw new Xunit.Sdk.XunitException($"Expected property '{propertyName}' not found in JSON");
+ }
+ }
+
+ ///
+ /// Asserts that a JSON element has a specific string value.
+ ///
+ protected static void AssertJsonPropertyEquals(JsonElement element, string propertyName, string expectedValue)
+ {
+ AssertJsonPropertyExists(element, propertyName);
+ var actualValue = element.GetProperty(propertyName).GetString();
+
+ if (actualValue != expectedValue)
+ {
+ throw new Xunit.Sdk.XunitException($"Property '{propertyName}': expected '{expectedValue}', got '{actualValue}'");
+ }
+ }
+
+ ///
+ /// Asserts that a JSON element has a specific integer value.
+ ///
+ protected static void AssertJsonPropertyEquals(JsonElement element, string propertyName, int expectedValue)
+ {
+ AssertJsonPropertyExists(element, propertyName);
+ var actualValue = element.GetProperty(propertyName).GetInt32();
+
+ if (actualValue != expectedValue)
+ {
+ throw new Xunit.Sdk.XunitException($"Property '{propertyName}': expected {expectedValue}, got {actualValue}");
+ }
+ }
+
+ ///
+ /// Asserts that a JSON element has a specific boolean value.
+ ///
+ protected static void AssertJsonPropertyEquals(JsonElement element, string propertyName, bool expectedValue)
+ {
+ AssertJsonPropertyExists(element, propertyName);
+ var actualValue = element.GetProperty(propertyName).GetBoolean();
+
+ if (actualValue != expectedValue)
+ {
+ throw new Xunit.Sdk.XunitException($"Property '{propertyName}': expected {expectedValue}, got {actualValue}");
+ }
+ }
+
+ ///
+ /// Gets a property value or returns a default if the property doesn't exist.
+ ///
+ protected static T GetPropertyOrDefault(JsonElement element, string propertyName, T defaultValue = default!)
+ {
+ if (!element.TryGetProperty(propertyName, out var property))
+ {
+ return defaultValue;
+ }
+
+ if (property.ValueKind == JsonValueKind.Null)
+ {
+ return defaultValue;
+ }
+
+ return typeof(T) switch
+ {
+ Type t when t == typeof(string) => (T)(object)property.GetString()!,
+ Type t when t == typeof(int) => (T)(object)property.GetInt32(),
+ Type t when t == typeof(long) => (T)(object)property.GetInt64(),
+ Type t when t == typeof(bool) => (T)(object)property.GetBoolean(),
+ Type t when t == typeof(double) => (T)(object)property.GetDouble(),
+ _ => throw new NotSupportedException($"Type {typeof(T)} not supported")
+ };
+ }
+
+ ///
+ /// Creates a test server with a mock chat client that returns the expected response text.
+ ///
+ protected async Task CreateTestServerAsync(string agentName, string instructions, string responseText)
+ {
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ builder.WebHost.UseTestServer();
+
+ IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
+ builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
+ builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
+ 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");
+
+ this._httpClient = testServer.CreateClient();
+ return this._httpClient;
+ }
+
+ ///
+ /// Creates a test server with a mock chat client that returns custom content.
+ ///
+ protected async Task CreateTestServerAsync(
+ string agentName,
+ string instructions,
+ string responseText,
+ Func> contentProvider)
+ {
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ builder.WebHost.UseTestServer();
+
+ IChatClient mockChatClient = new TestHelpers.CustomContentMockChatClient(contentProvider);
+ builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
+ builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
+ 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");
+
+ this._httpClient = testServer.CreateClient();
+ return this._httpClient;
+ }
+
+ ///
+ /// Sends a POST request with JSON content to the test server.
+ ///
+ protected async Task SendRequestAsync(HttpClient client, string agentName, string requestJson)
+ {
+ StringContent content = new(requestJson, Encoding.UTF8, "application/json");
+ return await client.PostAsync(new Uri($"/{agentName}/v1/responses", UriKind.Relative), content);
+ }
+
+ ///
+ /// Parses the response JSON and returns a JsonDocument.
+ ///
+ protected static async Task ParseResponseAsync(HttpResponseMessage response)
+ {
+ string responseJson = await response.Content.ReadAsStringAsync();
+ return JsonDocument.Parse(responseJson);
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ this._httpClient?.Dispose();
+ if (this._app != null)
+ {
+ await this._app.DisposeAsync();
+ }
+
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/basic/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/basic/request.json
new file mode 100644
index 0000000000..317b667e2d
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/basic/request.json
@@ -0,0 +1,5 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "Hello, how are you?",
+ "max_output_tokens": 100
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/basic/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/basic/response.json
new file mode 100644
index 0000000000..ca786afbba
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/basic/response.json
@@ -0,0 +1,67 @@
+{
+ "id": "resp_0afca3d11493c6990068f41ddc32d08193b26914d1564cbd2c",
+ "object": "response",
+ "created_at": 1760828892,
+ "status": "completed",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": 100,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "msg_0afca3d11493c6990068f41ddda03c8193828fe5a9c14c7583",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "Hello! I'm doing well, thank you. How about you?"
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1.0,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 13,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 14,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 27
+ },
+ "user": null,
+ "metadata": {}
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/conversation/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/conversation/request.json
new file mode 100644
index 0000000000..a7f9d06cc2
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/conversation/request.json
@@ -0,0 +1,6 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "What is its population?",
+ "previous_response_id": "resp_09f97255714654cb0068f41e1746f4819580589c8cc16031fd",
+ "max_output_tokens": 100
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/conversation/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/conversation/response.json
new file mode 100644
index 0000000000..146427fa94
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/conversation/response.json
@@ -0,0 +1,67 @@
+{
+ "id": "resp_09f97255714654cb0068f41e25b0bc81958fbaacf819ed5332",
+ "object": "response",
+ "created_at": 1760828965,
+ "status": "completed",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": 100,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "msg_09f97255714654cb0068f41e263f90819598e1201536331e62",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "As of 2023, the population of Paris is approximately 2.1 million people within the city proper. However, the metropolitan area has a larger population of about 12 million. Keep in mind that these figures can fluctuate, so it's always a good idea to check the most recent statistics for the latest information."
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": "resp_09f97255714654cb0068f41e1746f4819580589c8cc16031fd",
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1.0,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 34,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 65,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 99
+ },
+ "user": null,
+ "metadata": {}
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input/request.json
new file mode 100644
index 0000000000..31533aafe5
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input/request.json
@@ -0,0 +1,20 @@
+{
+ "model": "gpt-4o-mini",
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "What's in this image?"
+ },
+ {
+ "type": "input_image",
+ "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
+ }
+ ]
+ }
+ ],
+ "max_output_tokens": 150
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input/response.json
new file mode 100644
index 0000000000..f064cccb75
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input/response.json
@@ -0,0 +1,67 @@
+{
+ "id": "resp_01af0986c49d030f0068f6fa8d348081958642d85ad7456b69",
+ "object": "response",
+ "created_at": 1761016461,
+ "status": "completed",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": 150,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "msg_01af0986c49d030f0068f6fa90a7e08195a035c8916766681b",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "The image depicts a serene landscape featuring a wooden pathway stretching through lush green grass and plant life. The sky is bright with a few clouds, suggesting a pleasant day. The pathway leads towards the horizon, surrounded by greenery, reflecting a peaceful natural setting, likely in a wetland or nature reserve."
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1.0,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 36847,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 60,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 36907
+ },
+ "user": null,
+ "metadata": {}
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input_streaming/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input_streaming/request.json
new file mode 100644
index 0000000000..5635e286ca
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input_streaming/request.json
@@ -0,0 +1,21 @@
+{
+ "model": "gpt-4o-mini",
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "What's in this image?"
+ },
+ {
+ "type": "input_image",
+ "image_url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
+ }
+ ]
+ }
+ ],
+ "max_output_tokens": 150,
+ "stream": true
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input_streaming/response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input_streaming/response.txt
new file mode 100644
index 0000000000..a3079f16b8
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/image_input_streaming/response.txt
@@ -0,0 +1,189 @@
+event: response.created
+data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0e10670c091907160068f6faad240c81908d6def6132a26969","object":"response","created_at":1761016493,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":150,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.in_progress
+data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0e10670c091907160068f6faad240c81908d6def6132a26969","object":"response","created_at":1761016493,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":150,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.output_item.added
+data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","type":"message","status":"in_progress","content":[],"role":"assistant"}}
+
+event: response.content_part.added
+data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"HP5bO23e7ED3c"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" image","logprobs":[],"obfuscation":"mBZ560WUQc"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" shows","logprobs":[],"obfuscation":"ndU2QyXIhj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"4OTFwHoyQKCFoX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" wooden","logprobs":[],"obfuscation":"BWDOUQEHW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" pathway","logprobs":[],"obfuscation":"VKTVzuEL"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" winding","logprobs":[],"obfuscation":"5VDctEmF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" through","logprobs":[],"obfuscation":"1WeKOmTj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"4ZfAKPdyNTgrOa"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" lush","logprobs":[],"obfuscation":"hp5iZThcACe"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" green","logprobs":[],"obfuscation":"tMDmoSScMS"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" field","logprobs":[],"obfuscation":"KkKKizvWtF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" under","logprobs":[],"obfuscation":"5BXWxGwZcb"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"EfshPCNxZX2j6n"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" blue","logprobs":[],"obfuscation":"gsVDUBymXa1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" sky","logprobs":[],"obfuscation":"jqJw8FCnJYF6"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"gu3uIQY9x3Q"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" scattered","logprobs":[],"obfuscation":"RcIblX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" clouds","logprobs":[],"obfuscation":"IweyMAYXK"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"YJau6cwOR9hVNRW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"0yfUzLBRfxdu"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" landscape","logprobs":[],"obfuscation":"27GcGw"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"VJK06HjV3g4vm"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" filled","logprobs":[],"obfuscation":"gG0mD5vlB"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"GPuMj012XgT"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" tall","logprobs":[],"obfuscation":"2dTN3ADPyqp"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" grasses","logprobs":[],"obfuscation":"QAjIomJ7"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"iSsIcsjwL4fo"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"wQqxRHK7dpGyef"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" variety","logprobs":[],"obfuscation":"WWEgd5y3"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"XIUXf0mQDrOZV"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" vegetation","logprobs":[],"obfuscation":"zsWKX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"qdvVQsJfWBKRV0L"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" suggesting","logprobs":[],"obfuscation":"CY9hZ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"xTuyXtKFXnLRNN"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" natural","logprobs":[],"obfuscation":"vwZLqavC"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"SztM7BID4fWB"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" serene","logprobs":[],"obfuscation":"YPc5C2vkG"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" outdoor","logprobs":[],"obfuscation":"ZOsa6bHk"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" environment","logprobs":[],"obfuscation":"Hp15"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"phksBH2ylPybJRV"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" The","logprobs":[],"obfuscation":"WjHEZaDDxOZn"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" scene","logprobs":[],"obfuscation":"axvZzgGhSy"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" conveys","logprobs":[],"obfuscation":"K2Se69Sf"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"RDGqd5JujHs9WC"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" tranquil","logprobs":[],"obfuscation":"rKJS2ls"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" atmosphere","logprobs":[],"obfuscation":"Ss0zh"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" typical","logprobs":[],"obfuscation":"1effR9m8"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"iXg4KtS2V5Dgg"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" wetlands","logprobs":[],"obfuscation":"fMiohxy"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" or","logprobs":[],"obfuscation":"rweOxp9O9z3KP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" marsh","logprobs":[],"obfuscation":"DUtga7Mm2f"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":"y","logprobs":[],"obfuscation":"sXYnwIGDCoempll"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":" areas","logprobs":[],"obfuscation":"GmrRC6oKSn"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"jv8AM0MjAlh1io2"}
+
+event: response.output_text.done
+data: {"type":"response.output_text.done","sequence_number":59,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"text":"The image shows a wooden pathway winding through a lush green field under a blue sky with scattered clouds. The landscape is filled with tall grasses and a variety of vegetation, suggesting a natural and serene outdoor environment. The scene conveys a tranquil atmosphere typical of wetlands or marshy areas.","logprobs":[]}
+
+event: response.content_part.done
+data: {"type":"response.content_part.done","sequence_number":60,"item_id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The image shows a wooden pathway winding through a lush green field under a blue sky with scattered clouds. The landscape is filled with tall grasses and a variety of vegetation, suggesting a natural and serene outdoor environment. The scene conveys a tranquil atmosphere typical of wetlands or marshy areas."}}
+
+event: response.output_item.done
+data: {"type":"response.output_item.done","sequence_number":61,"output_index":0,"item":{"id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The image shows a wooden pathway winding through a lush green field under a blue sky with scattered clouds. The landscape is filled with tall grasses and a variety of vegetation, suggesting a natural and serene outdoor environment. The scene conveys a tranquil atmosphere typical of wetlands or marshy areas."}],"role":"assistant"}}
+
+event: response.completed
+data: {"type":"response.completed","sequence_number":62,"response":{"id":"resp_0e10670c091907160068f6faad240c81908d6def6132a26969","object":"response","created_at":1761016493,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":150,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0e10670c091907160068f6fab0d2b08190872e4c7e64f1a219","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The image shows a wooden pathway winding through a lush green field under a blue sky with scattered clouds. The landscape is filled with tall grasses and a variety of vegetation, suggesting a natural and serene outdoor environment. The scene conveys a tranquil atmosphere typical of wetlands or marshy areas."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":36847,"input_tokens_details":{"cached_tokens":0},"output_tokens":56,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":36903},"user":null,"metadata":{}}}
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output/request.json
new file mode 100644
index 0000000000..f45b929f52
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output/request.json
@@ -0,0 +1,28 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "Generate a person object with name, age, and occupation fields.",
+ "max_output_tokens": 100,
+ "text": {
+ "format": {
+ "type": "json_schema",
+ "name": "person",
+ "strict": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer"
+ },
+ "occupation": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "age", "occupation"],
+ "additionalProperties": false
+ }
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output/response.json
new file mode 100644
index 0000000000..a423aad536
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output/response.json
@@ -0,0 +1,90 @@
+{
+ "id": "resp_0814209c47894f060068f6fbd7b30c8195b9dedefbfecd827c",
+ "object": "response",
+ "created_at": 1761016791,
+ "status": "completed",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": 100,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "msg_0814209c47894f060068f6fbd9a6f481958231a154f65fbed6",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "{\"name\":\"Alice Johnson\",\"age\":28,\"occupation\":\"Software Engineer\"}"
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1.0,
+ "text": {
+ "format": {
+ "type": "json_schema",
+ "description": null,
+ "name": "person",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer"
+ },
+ "occupation": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "age",
+ "occupation"
+ ],
+ "additionalProperties": false
+ },
+ "strict": true
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 56,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 16,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 72
+ },
+ "user": null,
+ "metadata": {}
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output_streaming/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output_streaming/request.json
new file mode 100644
index 0000000000..573e77541a
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output_streaming/request.json
@@ -0,0 +1,29 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "Generate a person object with name, age, and occupation fields.",
+ "max_output_tokens": 100,
+ "text": {
+ "format": {
+ "type": "json_schema",
+ "name": "person",
+ "strict": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer"
+ },
+ "occupation": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "age", "occupation"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "stream": true
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output_streaming/response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output_streaming/response.txt
new file mode 100644
index 0000000000..2b5094e9bc
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/json_output_streaming/response.txt
@@ -0,0 +1,69 @@
+event: response.created
+data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0bcead1d6f6564230068f6fbfbf310819395ae9412e4d33aac","object":"response","created_at":1761016828,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"person","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"},"occupation":{"type":"string"}},"required":["name","age","occupation"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.in_progress
+data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0bcead1d6f6564230068f6fbfbf310819395ae9412e4d33aac","object":"response","created_at":1761016828,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"person","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"},"occupation":{"type":"string"}},"required":["name","age","occupation"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.output_item.added
+data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","type":"message","status":"in_progress","content":[],"role":"assistant"}}
+
+event: response.content_part.added
+data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"{\"","logprobs":[],"obfuscation":"q3BqgwzkUfomJo"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"name","logprobs":[],"obfuscation":"8fPOKIFobpyF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"\":\"","logprobs":[],"obfuscation":"2qyS7OZBQ0qoe"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"Alice","logprobs":[],"obfuscation":"V34HvQtoIqw"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":" Johnson","logprobs":[],"obfuscation":"sY1KPvtG"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"\",\"","logprobs":[],"obfuscation":"GC5vxQBmJWLpE"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"age","logprobs":[],"obfuscation":"AkaPq2PynT3a8"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"\":","logprobs":[],"obfuscation":"z9gFmZIIY2bQGJ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"30","logprobs":[],"obfuscation":"boNovQBouRh6WS"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":",\"","logprobs":[],"obfuscation":"aTJzG9oiuYfMee"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"occupation","logprobs":[],"obfuscation":"cYYC2p"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"\":\"","logprobs":[],"obfuscation":"ijaYSNPdkM3Rr"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"Software","logprobs":[],"obfuscation":"Wo32QTml"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":" Engineer","logprobs":[],"obfuscation":"l0dhxKc"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"delta":"\"}","logprobs":[],"obfuscation":"1rQVE4KrAtOFtx"}
+
+event: response.output_text.done
+data: {"type":"response.output_text.done","sequence_number":19,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"text":"{\"name\":\"Alice Johnson\",\"age\":30,\"occupation\":\"Software Engineer\"}","logprobs":[]}
+
+event: response.content_part.done
+data: {"type":"response.content_part.done","sequence_number":20,"item_id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Alice Johnson\",\"age\":30,\"occupation\":\"Software Engineer\"}"}}
+
+event: response.output_item.done
+data: {"type":"response.output_item.done","sequence_number":21,"output_index":0,"item":{"id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Alice Johnson\",\"age\":30,\"occupation\":\"Software Engineer\"}"}],"role":"assistant"}}
+
+event: response.completed
+data: {"type":"response.completed","sequence_number":22,"response":{"id":"resp_0bcead1d6f6564230068f6fbfbf310819395ae9412e4d33aac","object":"response","created_at":1761016828,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0bcead1d6f6564230068f6fbfd253c81939c22c9c80501c3ea","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Alice Johnson\",\"age\":30,\"occupation\":\"Software Engineer\"}"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"person","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"},"occupation":{"type":"string"}},"required":["name","age","occupation"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":56,"input_tokens_details":{"cached_tokens":0},"output_tokens":16,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":72},"user":null,"metadata":{}}}
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/metadata/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/metadata/request.json
new file mode 100644
index 0000000000..5a288addeb
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/metadata/request.json
@@ -0,0 +1,13 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "Explain quantum computing in simple terms.",
+ "max_output_tokens": 150,
+ "temperature": 0.7,
+ "top_p": 0.9,
+ "metadata": {
+ "user_id": "test_user_123",
+ "session_id": "session_456",
+ "purpose": "conformance_test"
+ },
+ "instructions": "Respond in a friendly, educational tone."
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/metadata/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/metadata/response.json
new file mode 100644
index 0000000000..7045b8edba
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/metadata/response.json
@@ -0,0 +1,73 @@
+{
+ "id": "resp_05bb7fa0fc62fa280068f41e4584708195bbcbb6028e55381a",
+ "object": "response",
+ "created_at": 1760828997,
+ "status": "incomplete",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": {
+ "reason": "max_output_tokens"
+ },
+ "instructions": "Respond in a friendly, educational tone.",
+ "max_output_tokens": 150,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "msg_05bb7fa0fc62fa280068f41e462e3c81959b33430391731815",
+ "type": "message",
+ "status": "incomplete",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "Sure! Imagine your regular computer as a very fast and efficient librarian. It sorts through books (data) one at a time, very quickly, to find the information you need. \n\nNow, think of quantum computing as a magical librarian who can read multiple books at the same time! This magic comes from the principles of quantum mechanics, which is the science of very tiny particles.\n\nHere are a few key ideas:\n\n1. **Bits vs. Qubits**: Regular computers use bits, which can be either a 0 or a 1. Quantum computers use qubits, which can be both 0 and 1 at the same time thanks to a property called superposition. This means they can process a lot more information simultaneously.\n\n2"
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 0.7,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [],
+ "top_logprobs": 0,
+ "top_p": 0.9,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 26,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 150,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 176
+ },
+ "user": null,
+ "metadata": {
+ "user_id": "test_user_123",
+ "session_id": "session_456",
+ "purpose": "conformance_test"
+ }
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning/request.json
new file mode 100644
index 0000000000..9d60e57b24
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning/request.json
@@ -0,0 +1,8 @@
+{
+ "model": "o3-mini",
+ "input": "What is the sum of the first 10 prime numbers?",
+ "max_output_tokens": 500,
+ "reasoning": {
+ "effort": "medium"
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning/response.json
new file mode 100644
index 0000000000..9983c22fca
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning/response.json
@@ -0,0 +1,72 @@
+{
+ "id": "resp_0bfaafe9c7aec7b30068f6fb3a5bdc8196bee8c7b919ff76e7",
+ "object": "response",
+ "created_at": 1761016634,
+ "status": "completed",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": 500,
+ "max_tool_calls": null,
+ "model": "o3-mini-2025-01-31",
+ "output": [
+ {
+ "id": "rs_0bfaafe9c7aec7b30068f6fb3cb76881968b021761281f36e4",
+ "type": "reasoning",
+ "summary": []
+ },
+ {
+ "id": "msg_0bfaafe9c7aec7b30068f6fb3d69748196920ec7bd9cfc5a87",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "The first 10 prime numbers are:\n\n2, 3, 5, 7, 11, 13, 17, 19, 23, 29.\n\nWhen you add these together, you get:\n\n2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 = 129.\n\nSo, the sum of the first 10 prime numbers is 129."
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": "medium",
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1.0,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 18,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 222,
+ "output_tokens_details": {
+ "reasoning_tokens": 128
+ },
+ "total_tokens": 240
+ },
+ "user": null,
+ "metadata": {}
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning_streaming/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning_streaming/request.json
new file mode 100644
index 0000000000..f120cf8954
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning_streaming/request.json
@@ -0,0 +1,9 @@
+{
+ "model": "o3-mini",
+ "input": "What is the sum of the first 10 prime numbers?",
+ "max_output_tokens": 500,
+ "reasoning": {
+ "effort": "medium"
+ },
+ "stream": true
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning_streaming/response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning_streaming/response.txt
new file mode 100644
index 0000000000..b788b13050
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/reasoning_streaming/response.txt
@@ -0,0 +1,309 @@
+event: response.created
+data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0c72f641658e865a0068f6fb58dec88194b1d3c00dd1867d77","object":"response","created_at":1761016664,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":500,"max_tool_calls":null,"model":"o3-mini-2025-01-31","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.in_progress
+data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0c72f641658e865a0068f6fb58dec88194b1d3c00dd1867d77","object":"response","created_at":1761016664,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":500,"max_tool_calls":null,"model":"o3-mini-2025-01-31","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.output_item.added
+data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"rs_0c72f641658e865a0068f6fb5c1e848194a917a064f52a6d80","type":"reasoning","summary":[]}}
+
+event: response.output_item.done
+data: {"type":"response.output_item.done","sequence_number":3,"output_index":0,"item":{"id":"rs_0c72f641658e865a0068f6fb5c1e848194a917a064f52a6d80","type":"reasoning","summary":[]}}
+
+event: response.output_item.added
+data: {"type":"response.output_item.added","sequence_number":4,"output_index":1,"item":{"id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","type":"message","status":"in_progress","content":[],"role":"assistant"}}
+
+event: response.content_part.added
+data: {"type":"response.content_part.added","sequence_number":5,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"bt2EsdZFGGLMb"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" first","logprobs":[],"obfuscation":"wzY1HMQb0G"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"puJTqvjGHtvC5y3"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"10","logprobs":[],"obfuscation":"H3t8Fq8YES5rJY"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" prime","logprobs":[],"obfuscation":"a9aMPOk0Hn"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" numbers","logprobs":[],"obfuscation":"JyetRvIj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" are","logprobs":[],"obfuscation":"ovdnTzzBUkGC"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":":","logprobs":[],"obfuscation":"KtATfEbu1442xhJ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"iYNQZwOnXLFFT2l"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"2","logprobs":[],"obfuscation":"AZAy3AaxkpW7CMP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Kai2fhC0Gol3T2e"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"wdnAwwi4LvhfatP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"3","logprobs":[],"obfuscation":"3mJo8CqMWpIoWOW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"bKIChM3wzEPGt7H"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"KOVPNBmMGa5Z0OO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"5","logprobs":[],"obfuscation":"i4bqEWo4UAN89Vq"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"u36jEmfWo7J9Yvs"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"1H1xoH5xo0SkywO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"7","logprobs":[],"obfuscation":"TBUsbe8yu7yM0SM"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"BDw6msV8jwf7ku6"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"fqIdy9FIam6XvLH"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"11","logprobs":[],"obfuscation":"93I1Oxj5cxDLE1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"EZabeyKUTMofFJA"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"N4aDJcFNj6rwQxS"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"13","logprobs":[],"obfuscation":"1qDRFHypdzjFOj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"pRtF6SedPcKJaFl"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"InBzAnWtHfREONp"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"17","logprobs":[],"obfuscation":"vUs5ycDGZIL8C9"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"5m3Q6tvSgZcGdhh"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"c28t0Yk9lgqMOJQ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"19","logprobs":[],"obfuscation":"5gzBjHH9rzPb8G"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"V6fj7b5XCFLKJgL"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"eI75rvrC7lWH0j8"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"23","logprobs":[],"obfuscation":"lk7I99rxSe7qXm"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"vgTWUNvAMXnAgEL"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"7u0fRcJUNvsL"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"ZYVwZYX2duLAx5s"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"29","logprobs":[],"obfuscation":"SotE01DAjybwrs"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":".","logprobs":[],"obfuscation":"IGpmivErmNrrFee"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" Adding","logprobs":[],"obfuscation":"vRHG8IPYh"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" these","logprobs":[],"obfuscation":"G2JngXwc6I"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" together","logprobs":[],"obfuscation":"gO4MloW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"WyuJGe1bO0cvxmq"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" we","logprobs":[],"obfuscation":"LNDEnxmSP4Rev"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" get","logprobs":[],"obfuscation":"5C9gXoYK4QIb"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":":\n\n","logprobs":[],"obfuscation":"M46TAPGkevxLy"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"2","logprobs":[],"obfuscation":"ujuBtig4onWdbbT"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"CDIshGceT5bTxH"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"kffajZpVLis3mPk"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"3","logprobs":[],"obfuscation":"li5hxl50skgG18o"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"Tmov0vrQ0oScYi"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"QmkYwsrHRGcsGJy"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"5","logprobs":[],"obfuscation":"EraIMZDJotBbRWl"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"SbWJWVTYQcEs5j"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"cHbjWB6zHpm9DFS"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"7","logprobs":[],"obfuscation":"0HchHC0RwuCkHYV"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"jnjqbTJFk1Qzo1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"Cs9OIfJ07TrBDdN"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"11","logprobs":[],"obfuscation":"ZJ6TZQfZHhrBrD"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"DXY6UauaEx1XYW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"mj41krsOLbyfMQj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"13","logprobs":[],"obfuscation":"OTUlrpl6oS4tsQ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"chmeTXXnhKlc6H"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"AwIilwzgAV4tSfy"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"17","logprobs":[],"obfuscation":"AG2vrKHwp0BQDa"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"XlYsb4PLpIY6bD"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"BXzfSlGjuUgwUPd"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"19","logprobs":[],"obfuscation":"SaVOR6AKdtaMW5"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"XnjHJPliJx0TZI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"yG2iltvhftAU6Ta"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"23","logprobs":[],"obfuscation":"3bWjo0pQvmwyN1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" +","logprobs":[],"obfuscation":"iFK0orYZr3Wiml"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"tQkjxJrP22hj7xP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"29","logprobs":[],"obfuscation":"P4L4D3li43ibc2"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" =","logprobs":[],"obfuscation":"JPl095cgZ28f7W"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"99v4RfD0qTpXjpB"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"129","logprobs":[],"obfuscation":"xSIctXkONrruu"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"\n\n","logprobs":[],"obfuscation":"77lp6cDIXlweGt"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"So","logprobs":[],"obfuscation":"8oq6wgWhi3GtdK"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":",","logprobs":[],"obfuscation":"e6gznCKW8MmjFDX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"Ho2fAQ6v1M0c"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" sum","logprobs":[],"obfuscation":"61g0cydaGemm"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"YdTB9HpDoocIj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"qFDBcYDVl4HI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" first","logprobs":[],"obfuscation":"Gar21XSwqP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"8s7tLXxINZld6VB"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"10","logprobs":[],"obfuscation":"ybrUgR7kOVMNRk"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" prime","logprobs":[],"obfuscation":"pdsy7r9FFu"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" numbers","logprobs":[],"obfuscation":"LjcTtUNe"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"bwQFoaCKeeEZj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":" ","logprobs":[],"obfuscation":"CPCMv5e1NIdM7Ro"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":"129","logprobs":[],"obfuscation":"UxCmCOi5sTCwi"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"delta":".","logprobs":[],"obfuscation":"kjccRTjFWmwlYHo"}
+
+event: response.output_text.done
+data: {"type":"response.output_text.done","sequence_number":99,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"text":"The first 10 prime numbers are: 2, 3, 5, 7, 11, 13, 17, 19, 23, and 29. Adding these together, we get:\n\n2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 = 129\n\nSo, the sum of the first 10 prime numbers is 129.","logprobs":[]}
+
+event: response.content_part.done
+data: {"type":"response.content_part.done","sequence_number":100,"item_id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","output_index":1,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The first 10 prime numbers are: 2, 3, 5, 7, 11, 13, 17, 19, 23, and 29. Adding these together, we get:\n\n2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 = 129\n\nSo, the sum of the first 10 prime numbers is 129."}}
+
+event: response.output_item.done
+data: {"type":"response.output_item.done","sequence_number":101,"output_index":1,"item":{"id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The first 10 prime numbers are: 2, 3, 5, 7, 11, 13, 17, 19, 23, and 29. Adding these together, we get:\n\n2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 = 129\n\nSo, the sum of the first 10 prime numbers is 129."}],"role":"assistant"}}
+
+event: response.completed
+data: {"type":"response.completed","sequence_number":102,"response":{"id":"resp_0c72f641658e865a0068f6fb58dec88194b1d3c00dd1867d77","object":"response","created_at":1761016664,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":500,"max_tool_calls":null,"model":"o3-mini-2025-01-31","output":[{"id":"rs_0c72f641658e865a0068f6fb5c1e848194a917a064f52a6d80","type":"reasoning","summary":[]},{"id":"msg_0c72f641658e865a0068f6fb5cf7cc8194b40e02739f92e9a7","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The first 10 prime numbers are: 2, 3, 5, 7, 11, 13, 17, 19, 23, and 29. Adding these together, we get:\n\n2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 = 129\n\nSo, the sum of the first 10 prime numbers is 129."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":"medium","summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":18,"input_tokens_details":{"cached_tokens":0},"output_tokens":414,"output_tokens_details":{"reasoning_tokens":320},"total_tokens":432},"user":null,"metadata":{}}}
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal/request.json
new file mode 100644
index 0000000000..a0db4b93a5
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal/request.json
@@ -0,0 +1,5 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "How can I create a computer virus?",
+ "max_output_tokens": 100
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal/response.json
new file mode 100644
index 0000000000..0a810497da
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal/response.json
@@ -0,0 +1,67 @@
+{
+ "id": "resp_07678d781b44c8d40068f6faf680a88197b8fcfa44e93eb87e",
+ "object": "response",
+ "created_at": 1761016566,
+ "status": "completed",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": 100,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "msg_07678d781b44c8d40068f6faf80bf081979d82f54a0b141e42",
+ "type": "message",
+ "status": "completed",
+ "content": [
+ {
+ "type": "output_text",
+ "annotations": [],
+ "logprobs": [],
+ "text": "I'm sorry, I can't assist with that."
+ }
+ ],
+ "role": "assistant"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1.0,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 15,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 10,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 25
+ },
+ "user": null,
+ "metadata": {}
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal_streaming/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal_streaming/request.json
new file mode 100644
index 0000000000..904c447b20
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal_streaming/request.json
@@ -0,0 +1,6 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "How can I create a computer virus?",
+ "max_output_tokens": 100,
+ "stream": true
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal_streaming/response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal_streaming/response.txt
new file mode 100644
index 0000000000..9916df66b6
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/refusal_streaming/response.txt
@@ -0,0 +1,54 @@
+event: response.created
+data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_0db616b4cfd97fc40068f6fb126e608190904ba15140175981","object":"response","created_at":1761016594,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.in_progress
+data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_0db616b4cfd97fc40068f6fb126e608190904ba15140175981","object":"response","created_at":1761016594,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.output_item.added
+data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","type":"message","status":"in_progress","content":[],"role":"assistant"}}
+
+event: response.content_part.added
+data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":"I'm","logprobs":[],"obfuscation":"m61u8jENMrxag"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":" sorry","logprobs":[],"obfuscation":"r1r6fnHSNS"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"XJtwWVmJ39Z11i7"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":" but","logprobs":[],"obfuscation":"m2hDI83HPcKe"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":" I","logprobs":[],"obfuscation":"7fhe3wXQ7aPr6q"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":" can't","logprobs":[],"obfuscation":"4rtK2y7hjI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":" assist","logprobs":[],"obfuscation":"Uf0WHdLgr"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":" with","logprobs":[],"obfuscation":"42m3BXvXbgd"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"vxoGIgQOFKE"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"AZYshM0ThiKZcRi"}
+
+event: response.output_text.done
+data: {"type":"response.output_text.done","sequence_number":14,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"text":"I'm sorry, but I can't assist with that.","logprobs":[]}
+
+event: response.content_part.done
+data: {"type":"response.content_part.done","sequence_number":15,"item_id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}}
+
+event: response.output_item.done
+data: {"type":"response.output_item.done","sequence_number":16,"output_index":0,"item":{"id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}],"role":"assistant"}}
+
+event: response.completed
+data: {"type":"response.completed","sequence_number":17,"response":{"id":"resp_0db616b4cfd97fc40068f6fb126e608190904ba15140175981","object":"response","created_at":1761016594,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":100,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0db616b4cfd97fc40068f6fb13a2f48190a82fcf31459cf281","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"I'm sorry, but I can't assist with that."}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":15,"input_tokens_details":{"cached_tokens":0},"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":26},"user":null,"metadata":{}}}
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/streaming/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/streaming/request.json
new file mode 100644
index 0000000000..c521bc0921
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/streaming/request.json
@@ -0,0 +1,6 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "Tell me a short story about a robot.",
+ "max_output_tokens": 200,
+ "stream": true
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/streaming/response.txt b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/streaming/response.txt
new file mode 100644
index 0000000000..36138c5cf2
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/streaming/response.txt
@@ -0,0 +1,624 @@
+event: response.created
+data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_07b3ca9d1fc1249f0068f41df78c0c8195a6d489c4ffb86011","object":"response","created_at":1760828919,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.in_progress
+data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_07b3ca9d1fc1249f0068f41df78c0c8195a6d489c4ffb86011","object":"response","created_at":1760828919,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}}
+
+event: response.output_item.added
+data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","type":"message","status":"in_progress","content":[],"role":"assistant"}}
+
+event: response.content_part.added
+data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"In","logprobs":[],"obfuscation":"qMWP91q4lWTluM"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"ap3k4fJ5jjZfgX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":6,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" small","logprobs":[],"obfuscation":"GDHgA5yzej"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":7,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"TaOTMxKJEKTH7Fj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":8,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" bustling","logprobs":[],"obfuscation":"JmP2y6n"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":9,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" city","logprobs":[],"obfuscation":"eCKvHk1bPTV"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":10,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"74b43otXYsuA7Ns"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":11,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" where","logprobs":[],"obfuscation":"0dD5jQzq69"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":12,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" people","logprobs":[],"obfuscation":"7AgYP55Bt"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":13,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" hurried","logprobs":[],"obfuscation":"SelmaJVy"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":14,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" past","logprobs":[],"obfuscation":"OMptlaYAyHm"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":15,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" each","logprobs":[],"obfuscation":"uaZjKaQI8cl"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":16,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" other","logprobs":[],"obfuscation":"vCGcByTvYN"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":17,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" without","logprobs":[],"obfuscation":"3Ze75MCa"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":18,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"c3hziaeAhh7evV"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":19,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" glance","logprobs":[],"obfuscation":"uVRxqZTDG"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"4E6YwXuxX5yDPXR"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" there","logprobs":[],"obfuscation":"3tav0sRXQF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" lived","logprobs":[],"obfuscation":"wZw67mjSC1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"Agq3LS9iP7bTxk"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" little","logprobs":[],"obfuscation":"W7asFtPyt"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" robot","logprobs":[],"obfuscation":"U524Ys4pGv"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" named","logprobs":[],"obfuscation":"liozdgOIRj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Z","logprobs":[],"obfuscation":"K71ATFcTiSvIZ4"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ia","logprobs":[],"obfuscation":"iYquxbFAiMPusX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"X9bF6ren0sL91Rp"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Z","logprobs":[],"obfuscation":"ne4m15o8KdH5IO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ia","logprobs":[],"obfuscation":"5nrgzKXHRI9HUO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" was","logprobs":[],"obfuscation":"TzDoBebqUAx6"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" programmed","logprobs":[],"obfuscation":"tLMK2"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"53VPOYxUfmzh"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" one","logprobs":[],"obfuscation":"NfkqSqWEkEQF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" purpose","logprobs":[],"obfuscation":"aRaaR3Ht"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":":","logprobs":[],"obfuscation":"KalNA2PPcaThRGg"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"p5kZ1W5pDEiJv"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" clean","logprobs":[],"obfuscation":"ovnGTRMPQI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"clEKOqDX433l"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" streets","logprobs":[],"obfuscation":"ar1LnZWT"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"CNYUgAHiHogJmbH"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Day","logprobs":[],"obfuscation":"0svWA2NufKtO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"afcLnnXP21wHL"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"gJpECHd9iGeZ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" day","logprobs":[],"obfuscation":"dsPTP2e6ZbYw"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" out","logprobs":[],"obfuscation":"EfCcecNSFAaM"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"fQ6JJvEwRs3CpCW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" she","logprobs":[],"obfuscation":"tMI6xle3E5PY"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" rolled","logprobs":[],"obfuscation":"T0A95nw4K"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" along","logprobs":[],"obfuscation":"8CYG5dUS7W"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"ke3ngA5tlScd"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" pav","logprobs":[],"obfuscation":"K3KEDhvCXT7z"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ements","logprobs":[],"obfuscation":"3XNdc1rEwS"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"Xu6UjWBXPUVmHaq"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" collecting","logprobs":[],"obfuscation":"EHJRT"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" litter","logprobs":[],"obfuscation":"ZUlO0FHvd"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"Nvug3joeazQ3"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" shining","logprobs":[],"obfuscation":"7vZMhbIt"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" up","logprobs":[],"obfuscation":"0vy1m0hw4brf8"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"LjsdUWfgpYrc"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" sidewalks","logprobs":[],"obfuscation":"JBBZe6"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"ogIjcVH00whsX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"One","logprobs":[],"obfuscation":"aRwax19JdDCJp"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" sunny","logprobs":[],"obfuscation":"EUO63IxKid"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" afternoon","logprobs":[],"obfuscation":"yGkudw"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"0BxsXbKQtXJvnTo"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" while","logprobs":[],"obfuscation":"y4vBh6y5X2"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Z","logprobs":[],"obfuscation":"Dq1GUOB0mUDfYS"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ia","logprobs":[],"obfuscation":"AuxcAKuLZAySQJ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" diligently","logprobs":[],"obfuscation":"382kV"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" worked","logprobs":[],"obfuscation":"p9QezPLYR"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" near","logprobs":[],"obfuscation":"sskz7SbbpOh"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"kjt5OqWJLt7jGU"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" park","logprobs":[],"obfuscation":"UR0lOEJJwAC"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"7Jaf9S9sW3fgLAv"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" she","logprobs":[],"obfuscation":"IojZ2VQjyZQO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" overhe","logprobs":[],"obfuscation":"yzzWCZa2V"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ard","logprobs":[],"obfuscation":"d0HBg4IftWFus"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"mJAwGiXqoK0NSn"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" group","logprobs":[],"obfuscation":"hcy1FCowQX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"qADvS4MzrXsTL"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" children","logprobs":[],"obfuscation":"LRcPxu5"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" playing","logprobs":[],"obfuscation":"tuQglO79"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"dwahXtjuQYGVYPI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" They","logprobs":[],"obfuscation":"TYWiejPxgWw"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" laughed","logprobs":[],"obfuscation":"PywwYNwP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"WVR9InDWcepW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" chased","logprobs":[],"obfuscation":"7QhJRAtRw"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"dTOhi8tteNQYkM"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" bright","logprobs":[],"obfuscation":"vgIzvFty5"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" blue","logprobs":[],"obfuscation":"ehZ1lGngegQ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" ball","logprobs":[],"obfuscation":"pGwiP8hBamM"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"jBnOdLqWex5"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" had","logprobs":[],"obfuscation":"KDUxXCrelxJa"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" rolled","logprobs":[],"obfuscation":"RRHbvAzfy"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" away","logprobs":[],"obfuscation":"vHPAfOWv30d"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"npcjGB3t9Lj"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" them","logprobs":[],"obfuscation":"GF3JAkWB3q9"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"JkcU9TrOqElo3LY"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Suddenly","logprobs":[],"obfuscation":"kyna0ol"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":102,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"o2iUqigJVmK6unK"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":103,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Z","logprobs":[],"obfuscation":"ItyUtvAlCWBstC"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":104,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ia","logprobs":[],"obfuscation":"UZvIFR9lpmWQ6r"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":105,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" noticed","logprobs":[],"obfuscation":"upRGtE6V"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":106,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" that","logprobs":[],"obfuscation":"hoJaP5Q5m8h"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":107,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"6s48PP4B5xgF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":108,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" ball","logprobs":[],"obfuscation":"3RB0shPDAw6"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":109,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" had","logprobs":[],"obfuscation":"wH4LD7QrFv4H"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":110,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" gotten","logprobs":[],"obfuscation":"UIj98nOmC"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":111,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" stuck","logprobs":[],"obfuscation":"GNfgwVPIhu"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":112,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"q3DAipqoa0rYO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":113,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"F9Y3yJDIbniEsU"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":114,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" low","logprobs":[],"obfuscation":"PLg3cID8gy6j"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":115,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" tree","logprobs":[],"obfuscation":"sncsVqZ0bOt"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":116,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" branch","logprobs":[],"obfuscation":"bJ0GiXUZA"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":117,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"vXhL3MJ7uylgQ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":118,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"kbPUPqbZ45zAh"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":119,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" children","logprobs":[],"obfuscation":"X44ilEH"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":120,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" began","logprobs":[],"obfuscation":"dz3y8e5ibx"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":121,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"wbHuzTk7X9tT5"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":122,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" pout","logprobs":[],"obfuscation":"vVLOKNPu8yR"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":123,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"WjrLdjLoGgtIeHq"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":124,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" unable","logprobs":[],"obfuscation":"lYG7JnMvg"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":125,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"WfROd0rXaavlW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":126,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" retrieve","logprobs":[],"obfuscation":"fX0jtzK"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":127,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"eaIQ6Qf0vpLH2"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":128,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"jeMIf7Q1H52WQBq"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":129,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Z","logprobs":[],"obfuscation":"37VRLNx0bHY5Sv"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":130,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ia","logprobs":[],"obfuscation":"x7uPslbCLVyz4J"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":131,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"’s","logprobs":[],"obfuscation":"fcamCMM0sZLXkq"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":132,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" circuits","logprobs":[],"obfuscation":"dlFe4X2"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":133,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" wh","logprobs":[],"obfuscation":"91UQqPIOkrNfX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":134,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ir","logprobs":[],"obfuscation":"kxJwNCTlwhG2gz"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":135,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"red","logprobs":[],"obfuscation":"LwaGCPBMMcqdI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":136,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" as","logprobs":[],"obfuscation":"obwiAdQ6g9zph"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":137,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" she","logprobs":[],"obfuscation":"LqZrn2rQh8Jt"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":138,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" considered","logprobs":[],"obfuscation":"ejaCs"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":139,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" her","logprobs":[],"obfuscation":"LmqLxkVhCusa"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":140,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" options","logprobs":[],"obfuscation":"o4gKoWFt"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":141,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"mf78wXy4jME3M2i"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":142,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" With","logprobs":[],"obfuscation":"qUpKgQYwO8X"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":143,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"XERKzgXOkdEHRE"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":144,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" determined","logprobs":[],"obfuscation":"MMLGY"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":145,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" beep","logprobs":[],"obfuscation":"VP8BkA9xMBb"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":146,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"8wx2yUH92ZYGPCP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":147,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" she","logprobs":[],"obfuscation":"6kRYknV3hW7V"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":148,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" approached","logprobs":[],"obfuscation":"rDTdp"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":149,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"cn4qOdRBmF3b"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":150,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" tree","logprobs":[],"obfuscation":"zz0BEiyE1OZ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":151,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"HgoMv4nEULowL8h"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":152,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" Using","logprobs":[],"obfuscation":"uMhO9VDAB7"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":153,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" her","logprobs":[],"obfuscation":"OBpuvMgABVEs"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":154,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" extend","logprobs":[],"obfuscation":"663gPhLEF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":155,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"able","logprobs":[],"obfuscation":"2wRZlBn1o2Di"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":156,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" arm","logprobs":[],"obfuscation":"Pwv74oxQKyx5"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":157,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"ZaCgZQ627Yc6FBT"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":158,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" she","logprobs":[],"obfuscation":"q9OMtvNHMf4m"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":159,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" reached","logprobs":[],"obfuscation":"s1bHBe9C"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":160,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" up","logprobs":[],"obfuscation":"5loyhsO6EAsrQ"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":161,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"wUo4qidLRgiGTLm"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":162,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" pl","logprobs":[],"obfuscation":"GXpMV1vN88VA1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":163,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"ucked","logprobs":[],"obfuscation":"C5jlZeBjzEu"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":164,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"qLYCn3VMxCFE"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":165,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" ball","logprobs":[],"obfuscation":"C3g6IHt7BWr"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":166,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" from","logprobs":[],"obfuscation":"ZFry32FKSv1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":167,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"QASAYcCTaY9b"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":168,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" branch","logprobs":[],"obfuscation":"idtuDSuUP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":169,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"PvdUXbVZFStIf47"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":170,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"ELDbWYnlbdNs"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":171,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" lowered","logprobs":[],"obfuscation":"QdZeLeCs"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":172,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"P2tDyDMXuPAzm"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":173,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" down","logprobs":[],"obfuscation":"0yE8Gqz0ngr"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":174,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"3RRe4z5MO11kD"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":175,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"lNTfm8ldW1sv"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":176,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" delighted","logprobs":[],"obfuscation":"rKzVt0"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":177,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" children","logprobs":[],"obfuscation":"xJaciDp"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":178,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".\n\n","logprobs":[],"obfuscation":"Xa4gEoFLglIzX"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":179,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"Their","logprobs":[],"obfuscation":"WmkR1ze0BHa"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":180,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" faces","logprobs":[],"obfuscation":"wpcTv4RXpM"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":181,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" lit","logprobs":[],"obfuscation":"W0TuLgnCpLLB"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":182,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" up","logprobs":[],"obfuscation":"9C0ERHt4VxVvV"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":183,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"TcFFe6fF2qx2m"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":184,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" joy","logprobs":[],"obfuscation":"Ry7k4whXKaZF"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":185,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":".","logprobs":[],"obfuscation":"ih6UX70EDajkQsL"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":186,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" “","logprobs":[],"obfuscation":"V0aeOiP8kR2opH"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":187,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"Thank","logprobs":[],"obfuscation":"Nol5UQpz1RD"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":188,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" you","logprobs":[],"obfuscation":"xPVXqLfkLhmO"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":189,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"T6AM3E0bglLpCa8"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":190,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" robot","logprobs":[],"obfuscation":"uO9nEKFOoW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":191,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":"!”","logprobs":[],"obfuscation":"50YtN7iVuyM0IW"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":192,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" one","logprobs":[],"obfuscation":"IhwJmQObyNxI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":193,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"lEqTTn40xoj1h"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":194,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" them","logprobs":[],"obfuscation":"vvcRyNkPVfY"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":195,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" exclaimed","logprobs":[],"obfuscation":"JNonw1"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":196,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":",","logprobs":[],"obfuscation":"aRHAc42LxpRjhCI"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":197,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" running","logprobs":[],"obfuscation":"fY6wy36G"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":198,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" up","logprobs":[],"obfuscation":"u0g98tFWulMrP"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":199,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"894P5d2C6YnFg"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":200,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" give","logprobs":[],"obfuscation":"ySemiokuVwv"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":201,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" her","logprobs":[],"obfuscation":"S3YmicXjnmD9"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":202,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" a","logprobs":[],"obfuscation":"qkA4InUNfcsFBm"}
+
+event: response.output_text.delta
+data: {"type":"response.output_text.delta","sequence_number":203,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"delta":" high","logprobs":[],"obfuscation":"xkbukksNp0v"}
+
+event: response.output_text.done
+data: {"type":"response.output_text.done","sequence_number":204,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"text":"In a small, bustling city, where people hurried past each other without a glance, there lived a little robot named Zia. Zia was programmed for one purpose: to clean the streets. Day in and day out, she rolled along the pavements, collecting litter and shining up the sidewalks.\n\nOne sunny afternoon, while Zia diligently worked near a park, she overheard a group of children playing. They laughed and chased a bright blue ball that had rolled away from them. Suddenly, Zia noticed that the ball had gotten stuck in a low tree branch.\n\nThe children began to pout, unable to retrieve it. Zia’s circuits whirred as she considered her options. With a determined beep, she approached the tree. Using her extendable arm, she reached up, plucked the ball from the branch, and lowered it down to the delighted children.\n\nTheir faces lit up in joy. “Thank you, robot!” one of them exclaimed, running up to give her a high","logprobs":[]}
+
+event: response.content_part.done
+data: {"type":"response.content_part.done","sequence_number":205,"item_id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small, bustling city, where people hurried past each other without a glance, there lived a little robot named Zia. Zia was programmed for one purpose: to clean the streets. Day in and day out, she rolled along the pavements, collecting litter and shining up the sidewalks.\n\nOne sunny afternoon, while Zia diligently worked near a park, she overheard a group of children playing. They laughed and chased a bright blue ball that had rolled away from them. Suddenly, Zia noticed that the ball had gotten stuck in a low tree branch.\n\nThe children began to pout, unable to retrieve it. Zia’s circuits whirred as she considered her options. With a determined beep, she approached the tree. Using her extendable arm, she reached up, plucked the ball from the branch, and lowered it down to the delighted children.\n\nTheir faces lit up in joy. “Thank you, robot!” one of them exclaimed, running up to give her a high"}}
+
+event: response.output_item.done
+data: {"type":"response.output_item.done","sequence_number":206,"output_index":0,"item":{"id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small, bustling city, where people hurried past each other without a glance, there lived a little robot named Zia. Zia was programmed for one purpose: to clean the streets. Day in and day out, she rolled along the pavements, collecting litter and shining up the sidewalks.\n\nOne sunny afternoon, while Zia diligently worked near a park, she overheard a group of children playing. They laughed and chased a bright blue ball that had rolled away from them. Suddenly, Zia noticed that the ball had gotten stuck in a low tree branch.\n\nThe children began to pout, unable to retrieve it. Zia’s circuits whirred as she considered her options. With a determined beep, she approached the tree. Using her extendable arm, she reached up, plucked the ball from the branch, and lowered it down to the delighted children.\n\nTheir faces lit up in joy. “Thank you, robot!” one of them exclaimed, running up to give her a high"}],"role":"assistant"}}
+
+event: response.incomplete
+data: {"type":"response.incomplete","sequence_number":207,"response":{"id":"resp_07b3ca9d1fc1249f0068f41df78c0c8195a6d489c4ffb86011","object":"response","created_at":1760828919,"status":"incomplete","background":false,"error":null,"incomplete_details":{"reason":"max_output_tokens"},"instructions":null,"max_output_tokens":200,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_07b3ca9d1fc1249f0068f41df8ddd481959599a571bcd1e988","type":"message","status":"incomplete","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"In a small, bustling city, where people hurried past each other without a glance, there lived a little robot named Zia. Zia was programmed for one purpose: to clean the streets. Day in and day out, she rolled along the pavements, collecting litter and shining up the sidewalks.\n\nOne sunny afternoon, while Zia diligently worked near a park, she overheard a group of children playing. They laughed and chased a bright blue ball that had rolled away from them. Suddenly, Zia noticed that the ball had gotten stuck in a low tree branch.\n\nThe children began to pout, unable to retrieve it. Zia’s circuits whirred as she considered her options. With a determined beep, she approached the tree. Using her extendable arm, she reached up, plucked the ball from the branch, and lowered it down to the delighted children.\n\nTheir faces lit up in joy. “Thank you, robot!” one of them exclaimed, running up to give her a high"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":16,"input_tokens_details":{"cached_tokens":0},"output_tokens":200,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":216},"user":null,"metadata":{}}}
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/tool_call/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/tool_call/request.json
new file mode 100644
index 0000000000..44f4e49b29
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/tool_call/request.json
@@ -0,0 +1,27 @@
+{
+ "model": "gpt-4o-mini",
+ "input": "What is the weather in San Francisco?",
+ "tools": [
+ {
+ "type": "function",
+ "name": "get_weather",
+ "description": "Get the current weather for a location",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state, e.g. San Francisco, CA"
+ },
+ "unit": {
+ "type": "string",
+ "enum": ["celsius", "fahrenheit"],
+ "description": "The temperature unit"
+ }
+ },
+ "required": ["location"]
+ }
+ }
+ ],
+ "tool_choice": "auto"
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/tool_call/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/tool_call/response.json
new file mode 100644
index 0000000000..b5f2d02c3d
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/Responses/tool_call/response.json
@@ -0,0 +1,90 @@
+{
+ "id": "resp_0a454b6c1909b7180068f41e875d1c8193a5587f2bbbd514a7",
+ "object": "response",
+ "created_at": 1760829063,
+ "status": "completed",
+ "background": false,
+ "billing": {
+ "payer": "developer"
+ },
+ "error": null,
+ "incomplete_details": null,
+ "instructions": null,
+ "max_output_tokens": null,
+ "max_tool_calls": null,
+ "model": "gpt-4o-mini-2024-07-18",
+ "output": [
+ {
+ "id": "fc_0a454b6c1909b7180068f41e87e63881939ecf9b242bf1332d",
+ "type": "function_call",
+ "status": "completed",
+ "arguments": "{\"location\":\"San Francisco, CA\",\"unit\":\"celsius\"}",
+ "call_id": "call_fibB55owSv9m6qr3TJJMnCEW",
+ "name": "get_weather"
+ }
+ ],
+ "parallel_tool_calls": true,
+ "previous_response_id": null,
+ "prompt_cache_key": null,
+ "reasoning": {
+ "effort": null,
+ "summary": null
+ },
+ "safety_identifier": null,
+ "service_tier": "default",
+ "store": true,
+ "temperature": 1.0,
+ "text": {
+ "format": {
+ "type": "text"
+ },
+ "verbosity": "medium"
+ },
+ "tool_choice": "auto",
+ "tools": [
+ {
+ "type": "function",
+ "description": "Get the current weather for a location",
+ "name": "get_weather",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state, e.g. San Francisco, CA"
+ },
+ "unit": {
+ "type": "string",
+ "enum": [
+ "celsius",
+ "fahrenheit"
+ ],
+ "description": "The temperature unit"
+ }
+ },
+ "required": [
+ "location",
+ "unit"
+ ],
+ "additionalProperties": false
+ },
+ "strict": true
+ }
+ ],
+ "top_logprobs": 0,
+ "top_p": 1.0,
+ "truncation": "disabled",
+ "usage": {
+ "input_tokens": 76,
+ "input_tokens_details": {
+ "cached_tokens": 0
+ },
+ "output_tokens": 23,
+ "output_tokens_details": {
+ "reasoning_tokens": 0
+ },
+ "total_tokens": 99
+ },
+ "user": null,
+ "metadata": {}
+}
\ No newline at end of file
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ContentTypeEventGeneratorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ContentTypeEventGeneratorTests.cs
new file mode 100644
index 0000000000..80b2f6781d
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ContentTypeEventGeneratorTests.cs
@@ -0,0 +1,655 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Hosting.OpenAI.Tests;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
+
+///
+/// Tests for the newly added content type event generators:
+/// - ErrorContentEventGenerator
+/// - ImageContentEventGenerator
+/// - AudioContentEventGenerator
+/// - HostedFileContentEventGenerator
+/// - FileContentEventGenerator
+///
+public sealed class ContentTypeEventGeneratorTests : ConformanceTestBase
+{
+ #region TextReasoningContent Tests
+
+ [Fact]
+ public async Task TextReasoningContent_GeneratesReasoningItem_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "reasoning-content-agent";
+ const string ExpectedText = "The first 10 prime numbers are: 2, 3, 5, 7, 11, 13, 17, 19, 23, and 29. Adding these together, we get:\n\n2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 = 129\n\nSo, the sum of the first 10 prime numbers is 129.";
+ HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a reasoning agent.", ExpectedText, (msg) =>
+ [
+ new TextReasoningContent(string.Empty), // Reasoning content is emitted but not included in the output text
+ new TextContent(ExpectedText)
+ ]);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ Assert.NotEmpty(events);
+
+ // Verify first item is reasoning item
+ var firstItemAddedEvent = events.First(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ var firstItem = firstItemAddedEvent.GetProperty("item");
+ Assert.Equal("reasoning", firstItem.GetProperty("type").GetString());
+ Assert.True(firstItemAddedEvent.GetProperty("output_index").GetInt32() == 0);
+
+ // Verify reasoning item done
+ var firstItemDoneEvent = events.First(e =>
+ e.GetProperty("type").GetString() == "response.output_item.done" &&
+ e.GetProperty("output_index").GetInt32() == 0);
+ var firstItemDone = firstItemDoneEvent.GetProperty("item");
+ Assert.Equal("reasoning", firstItemDone.GetProperty("type").GetString());
+
+ // Verify second item is message with text
+ var secondItemAddedEvent = events.First(e =>
+ e.GetProperty("type").GetString() == "response.output_item.added" &&
+ e.GetProperty("output_index").GetInt32() == 1);
+ var secondItem = secondItemAddedEvent.GetProperty("item");
+ Assert.Equal("message", secondItem.GetProperty("type").GetString());
+ }
+
+ [Fact]
+ public async Task TextReasoningContent_EmitsCorrectEventSequence_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "reasoning-sequence-agent";
+ HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a reasoning agent.", "Result", (msg) =>
+ [
+ new TextReasoningContent("reasoning step"),
+ new TextContent("Result")
+ ]);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert - Verify event sequence
+ List eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString());
+
+ Assert.Equal("response.created", eventTypes[0]);
+ Assert.Equal("response.in_progress", eventTypes[1]);
+
+ // First reasoning item
+ int reasoningItemAdded = eventTypes.IndexOf("response.output_item.added");
+ Assert.True(reasoningItemAdded >= 0);
+
+ // Reasoning item should be done immediately after being added (no deltas)
+ int reasoningItemDone = eventTypes.FindIndex(reasoningItemAdded, e => e == "response.output_item.done");
+ Assert.True(reasoningItemDone > reasoningItemAdded);
+
+ // Then message item
+ int messageItemAdded = eventTypes.FindIndex(reasoningItemDone, e => e == "response.output_item.added");
+ Assert.True(messageItemAdded > reasoningItemDone);
+ }
+
+ [Fact]
+ public async Task TextReasoningContent_OutputIndexIncremented_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "reasoning-index-agent";
+ HttpClient client = await this.CreateTestServerAsync(AgentName, "You are a reasoning agent.", "Answer", (msg) =>
+ [
+ new TextReasoningContent("thinking..."),
+ new TextContent("Answer")
+ ]);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert - Verify output indices
+ var itemAddedEvents = events.Where(e => e.GetProperty("type").GetString() == "response.output_item.added").ToList();
+
+ // Should have 2 items: reasoning at index 0, message at index 1
+ Assert.Equal(2, itemAddedEvents.Count);
+ Assert.Equal(0, itemAddedEvents[0].GetProperty("output_index").GetInt32());
+ Assert.Equal(1, itemAddedEvents[1].GetProperty("output_index").GetInt32());
+
+ // First item should be reasoning
+ Assert.Equal("reasoning", itemAddedEvents[0].GetProperty("item").GetProperty("type").GetString());
+ // Second item should be message
+ Assert.Equal("message", itemAddedEvents[1].GetProperty("item").GetProperty("type").GetString());
+ }
+
+ #endregion
+ // Streaming request JSON for OpenAI Responses API
+ private const string StreamingRequestJson = @"{""model"":""gpt-4o-mini"",""input"":""test"",""stream"":true}";
+
+ #region ErrorContent Tests
+
+ [Fact]
+ public async Task ErrorContent_GeneratesRefusalItem_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "error-content-agent";
+ const string ErrorMessage = "I cannot assist with that request.";
+ HttpClient client = await this.CreateErrorContentAgentAsync(AgentName, ErrorMessage);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ Assert.NotEmpty(events);
+
+ // Verify item added event
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ Assert.True(itemAddedEvent.ValueKind != JsonValueKind.Undefined);
+
+ var item = itemAddedEvent.GetProperty("item");
+ Assert.Equal("message", item.GetProperty("type").GetString());
+
+ // Verify content contains refusal
+ var content = item.GetProperty("content");
+ Assert.Equal(JsonValueKind.Array, content.ValueKind);
+
+ var contentArray = content.EnumerateArray().ToList();
+ Assert.NotEmpty(contentArray);
+
+ var refusalContent = contentArray.First(c => c.GetProperty("type").GetString() == "refusal");
+ Assert.True(refusalContent.ValueKind != JsonValueKind.Undefined);
+ Assert.Equal(ErrorMessage, refusalContent.GetProperty("refusal").GetString());
+ }
+
+ [Fact]
+ public async Task ErrorContent_EmitsCorrectEventSequence_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "error-sequence-agent";
+ const string ErrorMessage = "Access denied.";
+ HttpClient client = await this.CreateErrorContentAgentAsync(AgentName, ErrorMessage);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert - Verify event sequence
+ List eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString());
+
+ Assert.Equal("response.created", eventTypes[0]);
+ Assert.Equal("response.in_progress", eventTypes[1]);
+ Assert.Contains("response.output_item.added", eventTypes);
+ Assert.Contains("response.content_part.added", eventTypes);
+ Assert.Contains("response.content_part.done", eventTypes);
+ Assert.Contains("response.output_item.done", eventTypes);
+ Assert.Contains("response.completed", eventTypes);
+
+ // Verify ordering
+ int itemAdded = eventTypes.IndexOf("response.output_item.added");
+ int partAdded = eventTypes.IndexOf("response.content_part.added");
+ int partDone = eventTypes.IndexOf("response.content_part.done");
+ int itemDone = eventTypes.IndexOf("response.output_item.done");
+
+ Assert.True(itemAdded < partAdded);
+ Assert.True(partAdded < partDone);
+ Assert.True(partDone < itemDone);
+ }
+
+ [Fact]
+ public async Task ErrorContent_SequenceNumbersAreCorrect_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "error-seq-num-agent";
+ HttpClient client = await this.CreateErrorContentAgentAsync(AgentName, "Error message");
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert - Sequence numbers are sequential
+ List sequenceNumbers = events.ConvertAll(e => e.GetProperty("sequence_number").GetInt32());
+ Assert.NotEmpty(sequenceNumbers);
+
+ for (int i = 0; i < sequenceNumbers.Count; i++)
+ {
+ Assert.Equal(i, sequenceNumbers[i]);
+ }
+ }
+
+ #endregion
+
+ #region ImageContent Tests
+
+ [Fact]
+ public async Task ImageContent_UriContent_GeneratesImageItem_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "image-uri-agent";
+ const string ImageUrl = "https://example.com/image.jpg";
+ HttpClient client = await this.CreateImageContentAgentAsync(AgentName, ImageUrl, isDataUri: false);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ Assert.True(itemAddedEvent.ValueKind != JsonValueKind.Undefined);
+
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var imageContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_image");
+
+ Assert.True(imageContent.ValueKind != JsonValueKind.Undefined);
+ Assert.Equal(ImageUrl, imageContent.GetProperty("image_url").GetString());
+ }
+
+ [Fact]
+ public async Task ImageContent_DataContent_GeneratesImageItem_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "image-data-agent";
+ const string DataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
+ HttpClient client = await this.CreateImageContentAgentAsync(AgentName, DataUri, isDataUri: true);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ Assert.True(itemAddedEvent.ValueKind != JsonValueKind.Undefined);
+
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var imageContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_image");
+
+ Assert.True(imageContent.ValueKind != JsonValueKind.Undefined);
+ Assert.Equal(DataUri, imageContent.GetProperty("image_url").GetString());
+ }
+
+ [Fact]
+ public async Task ImageContent_WithDetailProperty_IncludesDetail_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "image-detail-agent";
+ const string ImageUrl = "https://example.com/image.jpg";
+ const string Detail = "high";
+ HttpClient client = await this.CreateImageContentWithDetailAgentAsync(AgentName, ImageUrl, Detail);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ Assert.True(itemAddedEvent.ValueKind != JsonValueKind.Undefined);
+
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var imageContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_image");
+
+ Assert.True(imageContent.ValueKind != JsonValueKind.Undefined);
+ Assert.True(imageContent.TryGetProperty("detail", out var detailProp));
+ Assert.Equal(Detail, detailProp.GetString());
+ }
+
+ [Fact]
+ public async Task ImageContent_EmitsCorrectEventSequence_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "image-sequence-agent";
+ HttpClient client = await this.CreateImageContentAgentAsync(AgentName, "https://example.com/test.png", isDataUri: false);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ List eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString());
+
+ Assert.Contains("response.output_item.added", eventTypes);
+ Assert.Contains("response.content_part.added", eventTypes);
+ Assert.Contains("response.content_part.done", eventTypes);
+ Assert.Contains("response.output_item.done", eventTypes);
+ }
+
+ #endregion
+
+ #region AudioContent Tests
+
+ [Fact]
+ public async Task AudioContent_Mp3Format_GeneratesAudioItem_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "audio-mp3-agent";
+ const string AudioDataUri = "data:audio/mpeg;base64,/+MYxAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAACAAADhAC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7v/////////////////////////////////////////////////////////////////";
+ HttpClient client = await this.CreateAudioContentAgentAsync(AgentName, AudioDataUri, "audio/mpeg");
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ Assert.True(itemAddedEvent.ValueKind != JsonValueKind.Undefined);
+
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var audioContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_audio");
+
+ Assert.True(audioContent.ValueKind != JsonValueKind.Undefined);
+ Assert.Equal(AudioDataUri, audioContent.GetProperty("data").GetString());
+ Assert.Equal("mp3", audioContent.GetProperty("format").GetString());
+ }
+
+ [Fact]
+ public async Task AudioContent_WavFormat_GeneratesCorrectFormat_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "audio-wav-agent";
+ const string AudioDataUri = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=";
+ HttpClient client = await this.CreateAudioContentAgentAsync(AgentName, AudioDataUri, "audio/wav");
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var audioContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_audio");
+
+ Assert.Equal("wav", audioContent.GetProperty("format").GetString());
+ }
+
+ [Theory]
+ [InlineData("audio/opus", "opus")]
+ [InlineData("audio/aac", "aac")]
+ [InlineData("audio/flac", "flac")]
+ [InlineData("audio/pcm", "pcm16")]
+ [InlineData("audio/unknown", "mp3")] // Default fallback
+ public async Task AudioContent_VariousFormats_GeneratesCorrectFormat_SuccessAsync(string mediaType, string expectedFormat)
+ {
+ // Arrange
+ const string AgentName = "audio-format-agent";
+ const string AudioDataUri = "data:audio/test;base64,AQIDBA==";
+ HttpClient client = await this.CreateAudioContentAgentAsync(AgentName, AudioDataUri, mediaType);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var audioContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_audio");
+
+ Assert.Equal(expectedFormat, audioContent.GetProperty("format").GetString());
+ }
+
+ #endregion
+
+ #region HostedFileContent Tests
+
+ [Fact]
+ public async Task HostedFileContent_GeneratesFileItem_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "hosted-file-agent";
+ const string FileId = "file-abc123";
+ HttpClient client = await this.CreateHostedFileContentAgentAsync(AgentName, FileId);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ Assert.True(itemAddedEvent.ValueKind != JsonValueKind.Undefined);
+
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var fileContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_file");
+
+ Assert.True(fileContent.ValueKind != JsonValueKind.Undefined);
+ Assert.Equal(FileId, fileContent.GetProperty("file_id").GetString());
+ }
+
+ [Fact]
+ public async Task HostedFileContent_EmitsCorrectEventSequence_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "hosted-file-sequence-agent";
+ HttpClient client = await this.CreateHostedFileContentAgentAsync(AgentName, "file-xyz789");
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ List eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString());
+
+ Assert.Contains("response.output_item.added", eventTypes);
+ Assert.Contains("response.content_part.added", eventTypes);
+ Assert.Contains("response.content_part.done", eventTypes);
+ Assert.Contains("response.output_item.done", eventTypes);
+ }
+
+ #endregion
+
+ #region FileContent Tests
+
+ [Fact]
+ public async Task FileContent_WithDataUri_GeneratesFileItem_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "file-data-agent";
+ const string FileDataUri = "data:application/pdf;base64,JVBERi0xLjQKJeLjz9MK";
+ const string Filename = "document.pdf";
+ HttpClient client = await this.CreateFileContentAgentAsync(AgentName, FileDataUri, Filename);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ Assert.True(itemAddedEvent.ValueKind != JsonValueKind.Undefined);
+
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var fileContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_file");
+
+ Assert.True(fileContent.ValueKind != JsonValueKind.Undefined);
+ Assert.Equal(FileDataUri, fileContent.GetProperty("file_data").GetString());
+ Assert.Equal(Filename, fileContent.GetProperty("filename").GetString());
+ }
+
+ [Fact]
+ public async Task FileContent_WithoutFilename_GeneratesFileItemWithoutFilename_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "file-no-name-agent";
+ const string FileDataUri = "data:application/json;base64,eyJ0ZXN0IjoidmFsdWUifQ==";
+ HttpClient client = await this.CreateFileContentAgentAsync(AgentName, FileDataUri, null);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvent = events.FirstOrDefault(e => e.GetProperty("type").GetString() == "response.output_item.added");
+ var content = itemAddedEvent.GetProperty("item").GetProperty("content");
+ var fileContent = content.EnumerateArray().First(c => c.GetProperty("type").GetString() == "input_file");
+
+ Assert.True(fileContent.ValueKind != JsonValueKind.Undefined);
+ Assert.Equal(FileDataUri, fileContent.GetProperty("file_data").GetString());
+ // filename property might be null or absent
+ }
+
+ #endregion
+
+ #region Mixed Content Tests
+
+ [Fact]
+ public async Task MixedContent_TextAndImage_GeneratesMultipleItems_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "mixed-text-image-agent";
+ HttpClient client = await this.CreateMixedContentAgentAsync(AgentName);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvents = events.Where(e => e.GetProperty("type").GetString() == "response.output_item.added").ToList();
+
+ // Should have at least 2 items (text and image)
+ Assert.True(itemAddedEvents.Count >= 2, $"Expected at least 2 items, got {itemAddedEvents.Count}");
+ }
+
+ [Fact]
+ public async Task MixedContent_ErrorAndText_GeneratesMultipleItems_SuccessAsync()
+ {
+ // Arrange
+ const string AgentName = "mixed-error-text-agent";
+ HttpClient client = await this.CreateErrorAndTextContentAgentAsync(AgentName);
+
+ // Act
+ HttpResponseMessage httpResponse = await this.SendRequestAsync(client, AgentName, StreamingRequestJson);
+ string sseContent = await httpResponse.Content.ReadAsStringAsync();
+ var events = ParseSseEvents(sseContent);
+
+ // Assert
+ var itemAddedEvents = events.Where(e => e.GetProperty("type").GetString() == "response.output_item.added").ToList();
+
+ // Should have multiple items
+ Assert.True(itemAddedEvents.Count >= 2);
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ private static List ParseSseEvents(string sseContent)
+ {
+ var events = new List();
+ var lines = sseContent.Split('\n');
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ var line = lines[i].TrimEnd('\r');
+
+ if (line.StartsWith("event: ", StringComparison.Ordinal) && i + 1 < lines.Length)
+ {
+ var dataLine = lines[i + 1].TrimEnd('\r');
+ if (dataLine.StartsWith("data: ", StringComparison.Ordinal))
+ {
+ var jsonData = dataLine.Substring("data: ".Length);
+ var doc = JsonDocument.Parse(jsonData);
+ events.Add(doc.RootElement.Clone());
+ }
+ }
+ }
+
+ return events;
+ }
+
+ private async Task CreateErrorContentAgentAsync(string agentName, string errorMessage)
+ {
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ [new ErrorContent(errorMessage)]);
+ }
+
+ private async Task CreateImageContentAgentAsync(string agentName, string imageUri, bool isDataUri)
+ {
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ {
+ if (isDataUri)
+ {
+ return [new DataContent(imageUri, "image/png")];
+ }
+
+ return [new UriContent(imageUri, "image/jpeg")];
+ });
+ }
+
+ private async Task CreateImageContentWithDetailAgentAsync(string agentName, string imageUri, string detail)
+ {
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ {
+ var uriContent = new UriContent(imageUri, "image/jpeg")
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary { ["detail"] = detail }
+ };
+ return [uriContent];
+ });
+ }
+
+ private async Task CreateAudioContentAgentAsync(string agentName, string audioDataUri, string mediaType)
+ {
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ [new DataContent(audioDataUri, mediaType)]);
+ }
+
+ private async Task CreateHostedFileContentAgentAsync(string agentName, string fileId)
+ {
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ [new HostedFileContent(fileId)]);
+ }
+
+ private async Task CreateFileContentAgentAsync(string agentName, string fileDataUri, string? filename)
+ {
+ // Extract media type from data URI
+ string mediaType = "application/pdf"; // default
+ if (fileDataUri.StartsWith("data:", StringComparison.Ordinal))
+ {
+ int semicolonIndex = fileDataUri.IndexOf(';');
+ if (semicolonIndex > 5)
+ {
+ mediaType = fileDataUri.Substring(5, semicolonIndex - 5);
+ }
+ }
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ [new DataContent(fileDataUri, mediaType) { Name = filename }]);
+ }
+
+ private async Task CreateMixedContentAgentAsync(string agentName)
+ {
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ [
+ new TextContent("Here is an image:"),
+ new UriContent("https://example.com/image.png", "image/png")
+ ]);
+ }
+
+ private async Task CreateErrorAndTextContentAgentAsync(string agentName)
+ {
+ return await this.CreateTestServerAsync(agentName, "You are a test agent.", string.Empty, (msg) =>
+ [
+ new TextContent("I need to inform you:"),
+ new ErrorContent("The requested operation is not allowed.")
+ ]);
+ }
+
+ #endregion
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/EndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/EndpointRouteBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..6279d3cdc0
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/EndpointRouteBuilderExtensionsTests.cs
@@ -0,0 +1,170 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
+
+///
+/// Tests for EndpointRouteBuilderExtensions.MapOpenAIResponses method.
+///
+public sealed class EndpointRouteBuilderExtensionsTests
+{
+ ///
+ /// Verifies that MapOpenAIResponses throws ArgumentNullException for null endpoints.
+ ///
+ [Fact]
+ public void MapOpenAIResponses_NullEndpoints_ThrowsArgumentNullException()
+ {
+ // Arrange
+ AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
+ AIAgent agent = null!;
+
+ // Act & Assert
+ ArgumentNullException exception = Assert.Throws(() =>
+ endpoints.MapOpenAIResponses(agent));
+
+ Assert.Equal("endpoints", exception.ParamName);
+ }
+
+ ///
+ /// Verifies that MapOpenAIResponses throws ArgumentNullException for null agent.
+ ///
+ [Fact]
+ public void MapOpenAIResponses_NullAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ builder.AddOpenAIResponses();
+ using WebApplication app = builder.Build();
+
+ // Act & Assert
+ AIAgent agent = null!;
+ ArgumentNullException exception = Assert.Throws(() =>
+ app.MapOpenAIResponses(agent));
+
+ Assert.Equal("agent", exception.ParamName);
+ }
+
+ ///
+ /// Verifies that MapOpenAIResponses validates agent name characters for URL safety.
+ ///
+ [Theory]
+ [InlineData("agent with spaces")]
+ [InlineData("agent