// 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.Models; using Microsoft.Agents.AI.Hosting.OpenAI.Tests; namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; /// /// Tests for OpenAI Responses API model serialization and deserialization. /// These tests verify that our models correctly serialize to and deserialize from JSON /// matching the OpenAI wire format, without testing actual API implementation behavior. /// public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase { #region Request Serialization Tests [Fact] public void Deserialize_BasicRequest_Success() { // Arrange string json = LoadResponsesTraceFile("basic/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.Equal("gpt-4o-mini", request.Model); Assert.NotNull(request.Input); Assert.Equal(100, request.MaxOutputTokens); } [Fact] public void Deserialize_BasicRequest_RoundTrip() { // Arrange string originalJson = LoadResponsesTraceFile("basic/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.CreateResponse); string reserializedJson = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); CreateResponse? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(roundtripped); Assert.Equal(request.Model, roundtripped.Model); Assert.Equal(request.MaxOutputTokens, roundtripped.MaxOutputTokens); } [Fact] public void Deserialize_StreamingRequest_HasStreamFlag() { // Arrange string json = LoadResponsesTraceFile("streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.Equal(200, request.MaxOutputTokens); } [Fact] public void Deserialize_ConversationRequest_HasPreviousResponseId() { // Arrange string json = LoadResponsesTraceFile("conversation/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.PreviousResponseId); Assert.StartsWith("resp_", request.PreviousResponseId); } [Fact] public void Deserialize_MetadataRequest_HasAllParameters() { // Arrange string json = LoadResponsesTraceFile("metadata/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Metadata); Assert.Equal(3, request.Metadata.Count); Assert.Equal("test_user_123", request.Metadata["user_id"]); Assert.Equal("session_456", request.Metadata["session_id"]); Assert.Equal("conformance_test", request.Metadata["purpose"]); Assert.NotNull(request.Instructions); Assert.Equal("Respond in a friendly, educational tone.", request.Instructions); Assert.Equal(0.7, request.Temperature); Assert.Equal(0.9, request.TopP); Assert.Equal(150, request.MaxOutputTokens); } [Fact] public void Deserialize_ToolCallRequest_HasToolDefinitions() { // Arrange string json = LoadResponsesTraceFile("tool_call/request.json"); // Act // CreateResponse doesn't have Tools property - it uses dynamic JSON using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert Assert.True(root.TryGetProperty("tools", out var tools)); Assert.Equal(JsonValueKind.Array, tools.ValueKind); Assert.Equal(1, tools.GetArrayLength()); var tool = tools[0]; Assert.Equal("function", tool.GetProperty("type").GetString()); Assert.Equal("get_weather", tool.GetProperty("name").GetString()); Assert.True(tool.TryGetProperty("description", out _)); Assert.True(tool.TryGetProperty("parameters", out var parameters)); Assert.Equal("object", parameters.GetProperty("type").GetString()); } [Fact] public void Serialize_CreateMinimalRequest_MatchesFormat() { // Arrange var request = new CreateResponse { Model = "gpt-4o-mini", Input = ResponseInput.FromText("Hello") }; // Act string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert Assert.Equal("gpt-4o-mini", root.GetProperty("model").GetString()); Assert.True(root.TryGetProperty("input", out var input)); // Input can be string or object - verify one exists Assert.True(input.ValueKind is JsonValueKind.String or JsonValueKind.Object); } [Fact] public void Serialize_CreateRequestWithOptions_IncludesAllFields() { // Arrange var request = new CreateResponse { Model = "gpt-4o-mini", Input = ResponseInput.FromText("Test input"), MaxOutputTokens = 100, Temperature = 0.7, TopP = 0.9, Stream = false, Instructions = "Test instructions", Metadata = new Dictionary { ["key1"] = "value1", ["key2"] = "value2" } }; // Act string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert Assert.Equal("gpt-4o-mini", root.GetProperty("model").GetString()); Assert.Equal(100, root.GetProperty("max_output_tokens").GetInt32()); Assert.Equal(0.7, root.GetProperty("temperature").GetDouble()); Assert.Equal(0.9, root.GetProperty("top_p").GetDouble()); Assert.False(root.GetProperty("stream").GetBoolean()); Assert.Equal("Test instructions", root.GetProperty("instructions").GetString()); var metadata = root.GetProperty("metadata"); Assert.Equal("value1", metadata.GetProperty("key1").GetString()); Assert.Equal("value2", metadata.GetProperty("key2").GetString()); } [Fact] public void Serialize_NullableFields_AreOmittedWhenNull() { // Arrange var request = new CreateResponse { Model = "gpt-4o-mini", Input = ResponseInput.FromText("Test") }; // Act string json = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert - Optional fields should not be present when null Assert.False(root.TryGetProperty("previous_response_id", out _) && root.GetProperty("previous_response_id").ValueKind != JsonValueKind.Null, "previous_response_id should be omitted or null"); Assert.False(root.TryGetProperty("instructions", out _) && root.GetProperty("instructions").ValueKind != JsonValueKind.Null, "instructions should be omitted or null"); } [Fact] public void Deserialize_ImageInputRequest_HasImageData() { // Arrange string json = LoadResponsesTraceFile("image_input/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Input); } [Fact] public void Deserialize_ImageInputStreamingRequest_HasStreamAndImage() { // Arrange string json = LoadResponsesTraceFile("image_input_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Input); } [Fact] public void Deserialize_JsonOutputRequest_HasJsonSchema() { // Arrange string json = LoadResponsesTraceFile("json_output/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Input); Assert.NotNull(request.Text); Assert.NotNull(request.Text.Format); Assert.IsType(request.Text.Format); var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)request.Text.Format; Assert.Equal("json_schema", jsonSchemaFormat.Type); Assert.NotNull(jsonSchemaFormat.Name); Assert.NotEqual(default, jsonSchemaFormat.Schema); } [Fact] public void Deserialize_JsonOutputStreamingRequest_HasJsonSchemaAndStream() { // Arrange string json = LoadResponsesTraceFile("json_output_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Input); Assert.NotNull(request.Text); Assert.NotNull(request.Text.Format); Assert.IsType(request.Text.Format); var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)request.Text.Format; Assert.Equal("json_schema", jsonSchemaFormat.Type); } [Fact] public void Deserialize_ReasoningRequest_HasReasoningConfiguration() { // Arrange string json = LoadResponsesTraceFile("reasoning/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Reasoning); } [Fact] public void Deserialize_ReasoningStreamingRequest_HasReasoningAndStream() { // Arrange string json = LoadResponsesTraceFile("reasoning_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Reasoning); } [Fact] public void Deserialize_RefusalRequest_CanBeDeserialized() { // Arrange string json = LoadResponsesTraceFile("refusal/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Input); } [Fact] public void Deserialize_RefusalStreamingRequest_HasStream() { // Arrange string json = LoadResponsesTraceFile("refusal_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Input); } [Fact] public void Deserialize_InvalidInputObject_ThrowsHelpfulException() { // Arrange const string Json = "{\"model\":\"gpt-4o-mini\",\"input\":{\"input\":\"testing!\"},\"stream\":true}"; // Act & Assert var exception = Assert.Throws(() => JsonSerializer.Deserialize(Json, OpenAIHostingJsonContext.Default.CreateResponse)); Assert.Contains("ResponseInput must be either a string or an array of messages", exception.Message); Assert.Contains("Objects are not supported", exception.Message); } [Fact] public void Deserialize_AllRequests_CanBeDeserialized() { // Arrange string[] requestPaths = [ "basic/request.json", "streaming/request.json", "conversation/request.json", "metadata/request.json", "tool_call/request.json", "image_input/request.json", "image_input_streaming/request.json", "json_output/request.json", "json_output_streaming/request.json", "reasoning/request.json", "reasoning_streaming/request.json", "refusal/request.json", "refusal_streaming/request.json" ]; foreach (var path in requestPaths) { string json = LoadResponsesTraceFile(path); // Act & Assert - Should not throw CreateResponse? request = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.CreateResponse); Assert.NotNull(request); Assert.NotNull(request.Input); } } #endregion #region Response Deserialization Tests [Fact] public void Deserialize_BasicResponse_Success() { // Arrange string json = LoadResponsesTraceFile("basic/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.StartsWith("resp_", response.Id); Assert.Equal("response", response.Object); Assert.True(response.CreatedAt > 0); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Model); Assert.StartsWith("gpt-4o-mini", response.Model); } [Fact] public void Deserialize_BasicResponse_HasCorrectOutput() { // Arrange string json = LoadResponsesTraceFile("basic/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Output); Assert.Single(response.Output); var outputItem = response.Output[0]; Assert.NotNull(outputItem); // Verify it's a message type using var doc = JsonDocument.Parse(JsonSerializer.Serialize(outputItem, OpenAIHostingJsonContext.Default.ItemResource)); var root = doc.RootElement; Assert.Equal("message", root.GetProperty("type").GetString()); } [Fact] public void Deserialize_BasicResponse_HasCorrectUsage() { // Arrange string json = LoadResponsesTraceFile("basic/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Usage); Assert.True(response.Usage.InputTokens > 0); Assert.True(response.Usage.OutputTokens > 0); Assert.Equal(response.Usage.InputTokens + response.Usage.OutputTokens, response.Usage.TotalTokens); Assert.NotNull(response.Usage.InputTokensDetails); Assert.NotNull(response.Usage.OutputTokensDetails); } [Fact] public void Deserialize_ConversationResponse_HasPreviousResponseId() { // Arrange string json = LoadResponsesTraceFile("conversation/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.PreviousResponseId); Assert.StartsWith("resp_", response.PreviousResponseId); Assert.NotEqual(response.Id, response.PreviousResponseId); } [Fact] public void Deserialize_MetadataResponse_PreservesMetadata() { // Arrange string json = LoadResponsesTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Metadata); Assert.Equal("test_user_123", response.Metadata["user_id"]); Assert.Equal("session_456", response.Metadata["session_id"]); Assert.Equal("conformance_test", response.Metadata["purpose"]); } [Fact] public void Deserialize_MetadataResponse_HasIncompleteStatus() { // Arrange string json = LoadResponsesTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Incomplete, response.Status); Assert.NotNull(response.IncompleteDetails); Assert.Equal("max_output_tokens", response.IncompleteDetails.Reason); } [Fact] public void Deserialize_MetadataResponse_HasInstructions() { // Arrange string json = LoadResponsesTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Instructions); Assert.Equal("Respond in a friendly, educational tone.", response.Instructions); } [Fact] public void Deserialize_MetadataResponse_HasModelParameters() { // Arrange string json = LoadResponsesTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(0.7, response.Temperature); Assert.Equal(0.9, response.TopP); Assert.Equal(150, response.MaxOutputTokens); } [Fact] public void Deserialize_ToolCallResponse_HasFunctionCall() { // Arrange string json = LoadResponsesTraceFile("tool_call/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Output); Assert.Single(response.Output); // Verify the output is a function_call type using var doc = JsonDocument.Parse(JsonSerializer.Serialize(response.Output[0], OpenAIHostingJsonContext.Default.ItemResource)); var root = doc.RootElement; Assert.Equal("function_call", root.GetProperty("type").GetString()); Assert.Equal("get_weather", root.GetProperty("name").GetString()); Assert.True(root.TryGetProperty("arguments", out var args)); Assert.True(root.TryGetProperty("call_id", out var callId)); Assert.StartsWith("call_", callId.GetString()); } [Fact] public void Deserialize_ToolCallResponse_HasToolDefinitions() { // Arrange string json = LoadResponsesTraceFile("tool_call/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Tools); Assert.Single(response.Tools); var tool = response.Tools[0]; Assert.Equal(JsonValueKind.Object, tool.ValueKind); var toolObj = tool; Assert.Equal("function", toolObj.GetProperty("type").GetString()); Assert.Equal("get_weather", toolObj.GetProperty("name").GetString()); Assert.True(toolObj.TryGetProperty("parameters", out var parameters)); Assert.Equal("object", parameters.GetProperty("type").GetString()); } [Fact] public void Deserialize_ImageInputResponse_HasImageInInput() { // Arrange string json = LoadResponsesTraceFile("image_input/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); } [Fact] public void Deserialize_JsonOutputResponse_HasStructuredOutput() { // Arrange string json = LoadResponsesTraceFile("json_output/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); Assert.NotNull(response.Text); Assert.NotNull(response.Text.Format); Assert.IsType(response.Text.Format); var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)response.Text.Format; Assert.Equal("json_schema", jsonSchemaFormat.Type); } [Fact] public void Deserialize_ReasoningResponse_HasReasoningItems() { // Arrange string json = LoadResponsesTraceFile("reasoning/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); Assert.NotNull(response.Reasoning); } [Fact] public void Deserialize_RefusalResponse_HasRefusalContent() { // Arrange string json = LoadResponsesTraceFile("refusal/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); } [Fact] public void Deserialize_AllResponses_HaveRequiredFields() { // Arrange string[] responsePaths = [ "basic/response.json", "conversation/response.json", "metadata/response.json", "tool_call/response.json", "image_input/response.json", "json_output/response.json", "reasoning/response.json", "refusal/response.json" ]; foreach (var path in responsePaths) { string json = LoadResponsesTraceFile(path); // Act Response? response = JsonSerializer.Deserialize(json, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Id); Assert.Equal("response", response.Object); Assert.True(response.CreatedAt > 0, $"Response from {path} should have created_at"); Assert.NotNull(response.Model); Assert.NotNull(response.Output); } } [Fact] public void Deserialize_ResponseRoundTrip_PreservesData() { // Arrange string originalJson = LoadResponsesTraceFile("basic/response.json"); // Act - Deserialize and re-serialize Response? response = JsonSerializer.Deserialize(originalJson, OpenAIHostingJsonContext.Default.Response); string reserializedJson = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.Response); Response? roundtripped = JsonSerializer.Deserialize(reserializedJson, OpenAIHostingJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(roundtripped); Assert.Equal(response.Id, roundtripped.Id); Assert.Equal(response.CreatedAt, roundtripped.CreatedAt); Assert.Equal(response.Status, roundtripped.Status); Assert.Equal(response.Model, roundtripped.Model); } #endregion #region Streaming Event Deserialization Tests [Fact] public void ParseStreamingEvents_BasicFormat_Success() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); // Assert Assert.NotEmpty(events); Assert.All(events, evt => { Assert.True(evt.TryGetProperty("type", out var type)); Assert.True(evt.TryGetProperty("sequence_number", out var seqNum)); Assert.Equal(JsonValueKind.Number, seqNum.ValueKind); }); } [Fact] public void ParseStreamingEvents_HasCorrectEventTypes() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToHashSet(); // Assert Assert.Contains("response.created", eventTypes); Assert.Contains("response.in_progress", eventTypes); Assert.Contains("response.output_item.added", eventTypes); Assert.Contains("response.content_part.added", eventTypes); Assert.Contains("response.output_text.delta", eventTypes); Assert.Contains("response.output_text.done", eventTypes); Assert.Contains("response.content_part.done", eventTypes); Assert.Contains("response.output_item.done", eventTypes); } [Fact] public void ParseStreamingEvents_DeserializeCreatedEvent_Success() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var createdEventJson = events.First(e => e.GetProperty("type").GetString() == "response.created"); // Act string jsonString = createdEventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var created = (StreamingResponseCreated)evt; Assert.Equal(0, created.SequenceNumber); Assert.NotNull(created.Response); Assert.NotNull(created.Response.Id); Assert.StartsWith("resp_", created.Response.Id); } [Fact] public void ParseStreamingEvents_DeserializeInProgressEvent_Success() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var inProgressEventJson = events.First(e => e.GetProperty("type").GetString() == "response.in_progress"); // Act string jsonString = inProgressEventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var inProgress = (StreamingResponseInProgress)evt; Assert.Equal(1, inProgress.SequenceNumber); Assert.NotNull(inProgress.Response); Assert.Equal(ResponseStatus.InProgress, inProgress.Response.Status); } [Fact] public void ParseStreamingEvents_DeserializeOutputItemAdded_Success() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var itemAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.output_item.added"); // Act string jsonString = itemAddedJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var itemAdded = (StreamingOutputItemAdded)evt; Assert.Equal(0, itemAdded.OutputIndex); Assert.NotNull(itemAdded.Item); } [Fact] public void ParseStreamingEvents_DeserializeContentPartAdded_Success() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var partAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.content_part.added"); // Act string jsonString = partAddedJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var partAdded = (StreamingContentPartAdded)evt; Assert.NotNull(partAdded.ItemId); Assert.Equal(0, partAdded.OutputIndex); Assert.Equal(0, partAdded.ContentIndex); Assert.NotNull(partAdded.Part); } [Fact] public void ParseStreamingEvents_DeserializeTextDelta_Success() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var textDeltaJson = events.First(e => e.GetProperty("type").GetString() == "response.output_text.delta"); // Act string jsonString = textDeltaJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var textDelta = (StreamingOutputTextDelta)evt; Assert.NotNull(textDelta.ItemId); Assert.Equal(0, textDelta.OutputIndex); Assert.Equal(0, textDelta.ContentIndex); Assert.NotNull(textDelta.Delta); } [Fact] public void ParseStreamingEvents_AccumulateTextDeltas_MatchesFinalText() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); // Act var deltas = new List(); string? finalText = null; foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); if (evt is StreamingOutputTextDelta delta) { deltas.Add(delta.Delta); } else if (evt is StreamingOutputTextDone done) { finalText = done.Text; } } // Assert Assert.NotEmpty(deltas); Assert.NotNull(finalText); string accumulated = string.Concat(deltas); Assert.Equal(accumulated, finalText); } [Fact] public void ParseStreamingEvents_SequenceNumbersAreSequential() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); // Act var sequenceNumbers = new List(); foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); sequenceNumbers.Add(evt.SequenceNumber); } // Assert Assert.NotEmpty(sequenceNumbers); Assert.Equal(0, sequenceNumbers.First()); for (int i = 0; i < sequenceNumbers.Count; i++) { Assert.Equal(i, sequenceNumbers[i]); } } [Fact] public void ParseStreamingEvents_FinalEvent_IsTerminalState() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var lastEventJson = events.Last(); // Act string jsonString = lastEventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, OpenAIHostingJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); // Should be one of the terminal events bool isTerminal = evt is StreamingResponseCompleted or StreamingResponseIncomplete or StreamingResponseFailed; Assert.True(isTerminal, $"Expected terminal event, got: {evt.GetType().Name}"); } [Fact] public void ParseStreamingEvents_ImageInputStreaming_HasImageEvents() { // Arrange string sseContent = LoadResponsesTraceFile("image_input_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); // Assert Assert.NotEmpty(events); Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_JsonOutputStreaming_HasJsonSchemaEvents() { // Arrange string sseContent = LoadResponsesTraceFile("json_output_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); // Assert Assert.NotEmpty(events); Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_ReasoningStreaming_HasReasoningEvents() { // Arrange string sseContent = LoadResponsesTraceFile("reasoning_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToHashSet(); // Assert Assert.NotEmpty(events); // Should have reasoning-related events Assert.Contains("response.created", eventTypes); Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_RefusalStreaming_HasRefusalEvents() { // Arrange string sseContent = LoadResponsesTraceFile("refusal_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToHashSet(); // Assert Assert.NotEmpty(events); // Should have refusal-related events Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_AllStreamingTraces_CanBeDeserialized() { // Arrange string[] streamingPaths = [ "streaming/response.txt", "image_input_streaming/response.txt", "json_output_streaming/response.txt", "reasoning_streaming/response.txt", "refusal_streaming/response.txt" ]; foreach (var path in streamingPaths) { string sseContent = LoadResponsesTraceFile(path); // Act & Assert foreach (var eventJson in ParseSseEventsFromContent(sseContent)) { // Should not throw StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); } } } [Fact] public void ParseStreamingEvents_AllEvents_CanBeDeserialized() { // Arrange string sseContent = LoadResponsesTraceFile("streaming/response.txt"); // Act & Assert foreach (var eventJson in ParseSseEventsFromContent(sseContent)) { // Should not throw StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), OpenAIHostingJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); // Verify polymorphic deserialization worked Assert.True( evt is StreamingResponseCreated or StreamingResponseInProgress or StreamingResponseCompleted or StreamingResponseIncomplete or StreamingResponseFailed or StreamingOutputItemAdded or StreamingOutputItemDone or StreamingContentPartAdded or StreamingContentPartDone or StreamingOutputTextDelta or StreamingOutputTextDone or StreamingFunctionCallArgumentsDelta or StreamingFunctionCallArgumentsDone, $"Unknown event type: {evt.GetType().Name}"); } } /// /// Helper to parse SSE events from a streaming response content string. /// private static List ParseSseEventsFromContent(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)) { // Next line should have the data if (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; } #endregion }