File: OpenAIConversionTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.AI.OpenAI.Tests\Microsoft.Extensions.AI.OpenAI.Tests.csproj (Microsoft.Extensions.AI.OpenAI.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using OpenAI.Assistants;
using OpenAI.Chat;
using OpenAI.Realtime;
using OpenAI.Responses;
using Xunit;
 
namespace Microsoft.Extensions.AI;
 
public class OpenAIConversionTests
{
    private static readonly AIFunction _testFunction = AIFunctionFactory.Create(
        ([Description("The name parameter")] string name) => name,
        "test_function",
        "A test function for conversion");
 
    [Fact]
    public void AsOpenAIChatTool_ProducesValidInstance()
    {
        var tool = _testFunction.AsOpenAIChatTool();
 
        Assert.NotNull(tool);
        Assert.Equal("test_function", tool.FunctionName);
        Assert.Equal("A test function for conversion", tool.FunctionDescription);
        ValidateSchemaParameters(tool.FunctionParameters);
    }
 
    [Fact]
    public void AsOpenAIResponseTool_ProducesValidInstance()
    {
        var tool = _testFunction.AsOpenAIResponseTool();
 
        Assert.NotNull(tool);
    }
 
    [Fact]
    public void AsOpenAIConversationFunctionTool_ProducesValidInstance()
    {
        var tool = _testFunction.AsOpenAIConversationFunctionTool();
 
        Assert.NotNull(tool);
        Assert.Equal("test_function", tool.Name);
        Assert.Equal("A test function for conversion", tool.Description);
        ValidateSchemaParameters(tool.Parameters);
    }
 
    [Fact]
    public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance()
    {
        var tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition();
 
        Assert.NotNull(tool);
        Assert.Equal("test_function", tool.FunctionName);
        Assert.Equal("A test function for conversion", tool.Description);
        ValidateSchemaParameters(tool.Parameters);
    }
 
    /// <summary>Helper method to validate function parameters match our schema.</summary>
    private static void ValidateSchemaParameters(BinaryData parameters)
    {
        Assert.NotNull(parameters);
 
        using var jsonDoc = JsonDocument.Parse(parameters);
        var root = jsonDoc.RootElement;
 
        Assert.Equal("object", root.GetProperty("type").GetString());
        Assert.True(root.TryGetProperty("properties", out var properties));
        Assert.True(properties.TryGetProperty("name", out var nameProperty));
        Assert.Equal("string", nameProperty.GetProperty("type").GetString());
        Assert.Equal("The name parameter", nameProperty.GetProperty("description").GetString());
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
    {
        Assert.Throws<ArgumentNullException>("messages", () => ((IEnumerable<ChatMessage>)null!).AsOpenAIChatMessages());
 
        List<ChatMessage> messages =
        [
            new(ChatRole.System, "You are a helpful assistant."),
            new(ChatRole.User, "Hello"),
            new(ChatRole.Assistant,
            [
                new TextContent("Hi there!"),
                new FunctionCallContent("callid123", "SomeFunction", new Dictionary<string, object?>
                {
                    ["param1"] = "value1",
                    ["param2"] = 42
                }),
            ]),
            new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]),
            new(ChatRole.Assistant, "The answer is 42."),
        ];
 
        ChatOptions? options = withOptions ? new ChatOptions { Instructions = "You talk like a parrot." } : null;
 
        var convertedMessages = messages.AsOpenAIChatMessages(options).ToArray();
 
        int index = 0;
        if (withOptions)
        {
            Assert.Equal(6, convertedMessages.Length);
 
            index = 1;
            SystemChatMessage instructionsMessage = Assert.IsType<SystemChatMessage>(convertedMessages[0]);
            Assert.Equal("You talk like a parrot.", Assert.Single(instructionsMessage.Content).Text);
        }
        else
        {
            Assert.Equal(5, convertedMessages.Length);
        }
 
        SystemChatMessage m0 = Assert.IsType<SystemChatMessage>(convertedMessages[index]);
        Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text);
 
        UserChatMessage m1 = Assert.IsType<UserChatMessage>(convertedMessages[index + 1]);
        Assert.Equal("Hello", Assert.Single(m1.Content).Text);
 
        AssistantChatMessage m2 = Assert.IsType<AssistantChatMessage>(convertedMessages[index + 2]);
        Assert.Single(m2.Content);
        Assert.Equal("Hi there!", m2.Content[0].Text);
        var tc = Assert.Single(m2.ToolCalls);
        Assert.Equal("callid123", tc.Id);
        Assert.Equal("SomeFunction", tc.FunctionName);
        Assert.True(JsonElement.DeepEquals(JsonSerializer.SerializeToElement(new Dictionary<string, object?>
        {
            ["param1"] = "value1",
            ["param2"] = 42
        }), JsonSerializer.Deserialize<JsonElement>(tc.FunctionArguments.ToMemory().Span)));
 
        ToolChatMessage m3 = Assert.IsType<ToolChatMessage>(convertedMessages[index + 3]);
        Assert.Equal("callid123", m3.ToolCallId);
        Assert.Equal("theresult", Assert.Single(m3.Content).Text);
 
        AssistantChatMessage m4 = Assert.IsType<AssistantChatMessage>(convertedMessages[index + 4]);
        Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text);
    }
 
    [Fact]
    public void AsOpenAIResponseItems_ProducesExpectedOutput()
    {
        Assert.Throws<ArgumentNullException>("messages", () => ((IEnumerable<ChatMessage>)null!).AsOpenAIResponseItems());
 
        List<ChatMessage> messages =
        [
            new(ChatRole.System, "You are a helpful assistant."),
            new(ChatRole.User, "Hello"),
            new(ChatRole.Assistant,
            [
                new TextContent("Hi there!"),
                new FunctionCallContent("callid123", "SomeFunction", new Dictionary<string, object?>
                {
                    ["param1"] = "value1",
                    ["param2"] = 42
                }),
            ]),
            new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]),
            new(ChatRole.Assistant, "The answer is 42."),
        ];
 
        var convertedItems = messages.AsOpenAIResponseItems().ToArray();
 
        Assert.Equal(6, convertedItems.Length);
 
        MessageResponseItem m0 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[0]);
        Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text);
 
        MessageResponseItem m1 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[1]);
        Assert.Equal(OpenAI.Responses.MessageRole.User, m1.Role);
        Assert.Equal("Hello", Assert.Single(m1.Content).Text);
 
        MessageResponseItem m2 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[2]);
        Assert.Equal(OpenAI.Responses.MessageRole.Assistant, m2.Role);
        Assert.Equal("Hi there!", Assert.Single(m2.Content).Text);
 
        FunctionCallResponseItem m3 = Assert.IsAssignableFrom<FunctionCallResponseItem>(convertedItems[3]);
        Assert.Equal("callid123", m3.CallId);
        Assert.Equal("SomeFunction", m3.FunctionName);
        Assert.True(JsonElement.DeepEquals(JsonSerializer.SerializeToElement(new Dictionary<string, object?>
        {
            ["param1"] = "value1",
            ["param2"] = 42
        }), JsonSerializer.Deserialize<JsonElement>(m3.FunctionArguments.ToMemory().Span)));
 
        FunctionCallOutputResponseItem m4 = Assert.IsAssignableFrom<FunctionCallOutputResponseItem>(convertedItems[4]);
        Assert.Equal("callid123", m4.CallId);
        Assert.Equal("theresult", m4.FunctionOutput);
 
        MessageResponseItem m5 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[5]);
        Assert.Equal(OpenAI.Responses.MessageRole.Assistant, m5.Role);
        Assert.Equal("The answer is 42.", Assert.Single(m5.Content).Text);
    }
 
    [Fact]
    public void AsChatResponse_ConvertsOpenAIChatCompletion()
    {
        Assert.Throws<ArgumentNullException>("chatCompletion", () => ((ChatCompletion)null!).AsChatResponse());
 
        ChatCompletion cc = OpenAIChatModelFactory.ChatCompletion(
            "id", OpenAI.Chat.ChatFinishReason.Length, null, null,
            [ChatToolCall.CreateFunctionToolCall("id", "functionName", BinaryData.FromString("test"))],
            ChatMessageRole.User, null, null, null, new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
            "model123", null, OpenAIChatModelFactory.ChatTokenUsage(2, 1, 3));
        cc.Content.Add(ChatMessageContentPart.CreateTextPart("Hello, world!"));
        cc.Content.Add(ChatMessageContentPart.CreateImagePart(new Uri("http://example.com/image.png")));
 
        ChatResponse response = cc.AsChatResponse();
 
        Assert.Equal("id", response.ResponseId);
        Assert.Equal(ChatFinishReason.Length, response.FinishReason);
        Assert.Equal("model123", response.ModelId);
        Assert.Equal(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), response.CreatedAt);
        Assert.NotNull(response.Usage);
        Assert.Equal(1, response.Usage.InputTokenCount);
        Assert.Equal(2, response.Usage.OutputTokenCount);
        Assert.Equal(3, response.Usage.TotalTokenCount);
 
        ChatMessage message = Assert.Single(response.Messages);
        Assert.Equal(ChatRole.User, message.Role);
 
        Assert.Equal(3, message.Contents.Count);
        Assert.Equal("Hello, world!", Assert.IsType<TextContent>(message.Contents[0]).Text);
        Assert.Equal("http://example.com/image.png", Assert.IsType<UriContent>(message.Contents[1]).Uri.ToString());
        Assert.Equal("functionName", Assert.IsType<FunctionCallContent>(message.Contents[2]).Name);
    }
 
    [Fact]
    public async Task AsChatResponse_ConvertsOpenAIStreamingChatCompletionUpdates()
    {
        Assert.Throws<ArgumentNullException>("chatCompletionUpdates", () => ((IAsyncEnumerable<StreamingChatCompletionUpdate>)null!).AsChatResponseUpdatesAsync());
 
        List<ChatResponseUpdate> updates = [];
        await foreach (var update in CreateUpdates().AsChatResponseUpdatesAsync())
        {
            updates.Add(update);
        }
 
        ChatResponse response = updates.ToChatResponse();
 
        Assert.Equal("id", response.ResponseId);
        Assert.Equal(ChatFinishReason.ToolCalls, response.FinishReason);
        Assert.Equal("model123", response.ModelId);
        Assert.Equal(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero), response.CreatedAt);
        Assert.NotNull(response.Usage);
        Assert.Equal(1, response.Usage.InputTokenCount);
        Assert.Equal(2, response.Usage.OutputTokenCount);
        Assert.Equal(3, response.Usage.TotalTokenCount);
 
        ChatMessage message = Assert.Single(response.Messages);
        Assert.Equal(ChatRole.Assistant, message.Role);
 
        Assert.Equal(3, message.Contents.Count);
        Assert.Equal("Hello, world!", Assert.IsType<TextContent>(message.Contents[0]).Text);
        Assert.Equal("http://example.com/image.png", Assert.IsType<UriContent>(message.Contents[1]).Uri.ToString());
        Assert.Equal("functionName", Assert.IsType<FunctionCallContent>(message.Contents[2]).Name);
 
        static async IAsyncEnumerable<StreamingChatCompletionUpdate> CreateUpdates()
        {
            await Task.Yield();
            yield return OpenAIChatModelFactory.StreamingChatCompletionUpdate(
                "id",
                new ChatMessageContent(
                    ChatMessageContentPart.CreateTextPart("Hello, world!"),
                    ChatMessageContentPart.CreateImagePart(new Uri("http://example.com/image.png"))),
                null,
                [OpenAIChatModelFactory.StreamingChatToolCallUpdate(0, "id", ChatToolCallKind.Function, "functionName", BinaryData.FromString("test"))],
                ChatMessageRole.Assistant,
                null, null, null, OpenAI.Chat.ChatFinishReason.ToolCalls, new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
                "model123", null, OpenAIChatModelFactory.ChatTokenUsage(2, 1, 3));
        }
    }
 
    [Fact]
    public void AsChatResponse_ConvertsOpenAIResponse()
    {
        Assert.Throws<ArgumentNullException>("response", () => ((OpenAIResponse)null!).AsChatResponse());
 
        // The OpenAI library currently doesn't provide any way to create an OpenAIResponse instance,
        // as all constructors/factory methods currently are internal. Update this test when such functionality is available.
    }
 
    [Fact]
    public void AsChatResponseUpdatesAsync_ConvertsOpenAIStreamingResponseUpdates()
    {
        Assert.Throws<ArgumentNullException>("responseUpdates", () => ((IAsyncEnumerable<StreamingResponseUpdate>)null!).AsChatResponseUpdatesAsync());
 
        // The OpenAI library currently doesn't provide any way to create a StreamingResponseUpdate instance,
        // as all constructors/factory methods currently are internal. Update this test when such functionality is available.
    }
 
    [Fact]
    public void AsChatMessages_FromOpenAIChatMessages_ProducesExpectedOutput()
    {
        Assert.Throws<ArgumentNullException>("messages", () => ((IEnumerable<OpenAI.Chat.ChatMessage>)null!).AsChatMessages().ToArray());
 
        List<OpenAI.Chat.ChatMessage> openAIMessages =
        [
            new SystemChatMessage("You are a helpful assistant."),
            new UserChatMessage("Hello"),
            new AssistantChatMessage(ChatMessageContentPart.CreateTextPart("Hi there!")),
            new ToolChatMessage("call456", "Function output")
        ];
 
        var convertedMessages = openAIMessages.AsChatMessages().ToArray();
 
        Assert.Equal(4, convertedMessages.Length);
 
        Assert.Equal("You are a helpful assistant.", convertedMessages[0].Text);
        Assert.Equal("Hello", convertedMessages[1].Text);
        Assert.Equal("Hi there!", convertedMessages[2].Text);
        Assert.Equal("Function output", convertedMessages[3].Contents.OfType<FunctionResultContent>().First().Result);
    }
 
    [Fact]
    public void AsChatMessages_FromResponseItems_WithNullArgument_ThrowsArgumentNullException()
    {
        Assert.Throws<ArgumentNullException>("items", () => ((IEnumerable<ResponseItem>)null!).AsChatMessages());
    }
 
    [Fact]
    public void AsChatMessages_FromResponseItems_ProducesExpectedOutput()
    {
        List<ChatMessage> inputMessages =
        [
            new(ChatRole.Assistant, "Hi there!")
        ];
 
        var responseItems = inputMessages.AsOpenAIResponseItems().ToArray();
 
        var convertedMessages = responseItems.AsChatMessages().ToArray();
 
        Assert.Single(convertedMessages);
 
        var message = convertedMessages[0];
        Assert.Equal(ChatRole.Assistant, message.Role);
        Assert.Equal("Hi there!", message.Text);
    }
 
    [Fact]
    public void AsChatMessages_FromResponseItems_WithEmptyCollection_ReturnsEmptyCollection()
    {
        var convertedMessages = Array.Empty<ResponseItem>().AsChatMessages().ToArray();
        Assert.Empty(convertedMessages);
    }
 
    [Fact]
    public void AsChatMessages_FromResponseItems_WithFunctionCall_HandlesCorrectly()
    {
        List<ChatMessage> inputMessages =
        [
            new(ChatRole.Assistant,
            [
                new TextContent("I'll call a function."),
                new FunctionCallContent("call123", "TestFunction", new Dictionary<string, object?> { ["param"] = "value" })
            ])
        ];
 
        var responseItems = inputMessages.AsOpenAIResponseItems().ToArray();
        var convertedMessages = responseItems.AsChatMessages().ToArray();
 
        Assert.Single(convertedMessages);
 
        var message = convertedMessages[0];
        Assert.Equal(ChatRole.Assistant, message.Role);
 
        var textContent = message.Contents.OfType<TextContent>().FirstOrDefault();
        var functionCall = message.Contents.OfType<FunctionCallContent>().FirstOrDefault();
 
        Assert.NotNull(textContent);
        Assert.Equal("I'll call a function.", textContent.Text);
 
        Assert.NotNull(functionCall);
        Assert.Equal("call123", functionCall.CallId);
        Assert.Equal("TestFunction", functionCall.Name);
        Assert.Equal("value", functionCall.Arguments!["param"]?.ToString());
    }
 
    [Fact]
    public void AsOpenAIChatCompletion_WithNullArgument_ThrowsArgumentNullException()
    {
        Assert.Throws<ArgumentNullException>("response", () => ((ChatResponse)null!).AsOpenAIChatCompletion());
    }
 
    [Fact]
    public void AsOpenAIChatCompletion_WithMultipleContents_ProducesValidInstance()
    {
        var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant,
        [
            new TextContent("Here's an image and some text."),
            new UriContent("https://example.com/image.jpg", "image/jpeg"),
            new DataContent(new byte[] { 1, 2, 3, 4 }, "application/octet-stream")
        ]))
        {
            ResponseId = "multi-content-response",
            ModelId = "gpt-4-vision",
            FinishReason = ChatFinishReason.Stop,
            CreatedAt = new DateTimeOffset(2025, 1, 3, 14, 30, 0, TimeSpan.Zero),
            Usage = new UsageDetails
            {
                InputTokenCount = 25,
                OutputTokenCount = 12,
                TotalTokenCount = 37
            }
        };
 
        ChatCompletion completion = chatResponse.AsOpenAIChatCompletion();
 
        Assert.Equal("multi-content-response", completion.Id);
        Assert.Equal("gpt-4-vision", completion.Model);
        Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, completion.FinishReason);
        Assert.Equal(ChatMessageRole.Assistant, completion.Role);
        Assert.Equal(new DateTimeOffset(2025, 1, 3, 14, 30, 0, TimeSpan.Zero), completion.CreatedAt);
 
        Assert.NotNull(completion.Usage);
        Assert.Equal(25, completion.Usage.InputTokenCount);
        Assert.Equal(12, completion.Usage.OutputTokenCount);
        Assert.Equal(37, completion.Usage.TotalTokenCount);
 
        Assert.NotEmpty(completion.Content);
        Assert.Contains(completion.Content, c => c.Text == "Here's an image and some text.");
    }
 
    [Fact]
    public void AsOpenAIChatCompletion_WithEmptyData_HandlesGracefully()
    {
        var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello"));
        var completion = chatResponse.AsOpenAIChatCompletion();
 
        Assert.NotNull(completion);
        Assert.Equal(ChatMessageRole.Assistant, completion.Role);
        Assert.Equal("Hello", Assert.Single(completion.Content).Text);
        Assert.Empty(completion.ToolCalls);
 
        var emptyResponse = new ChatResponse([]);
        var emptyCompletion = emptyResponse.AsOpenAIChatCompletion();
        Assert.NotNull(emptyCompletion);
        Assert.Equal(ChatMessageRole.Assistant, emptyCompletion.Role);
    }
 
    [Fact]
    public void AsOpenAIChatCompletion_WithComplexFunctionCallArguments_SerializesCorrectly()
    {
        var complexArgs = new Dictionary<string, object?>
        {
            ["simpleString"] = "hello",
            ["number"] = 42,
            ["boolean"] = true,
            ["nullValue"] = null,
            ["nestedObject"] = new Dictionary<string, object?>
            {
                ["innerString"] = "world",
                ["innerArray"] = new[] { 1, 2, 3 }
            }
        };
 
        var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant,
        [
            new TextContent("I'll process this complex data."),
            new FunctionCallContent("process_data", "ProcessComplexData", complexArgs)
        ]))
        {
            ResponseId = "complex-function-call",
            ModelId = "gpt-4",
            FinishReason = ChatFinishReason.ToolCalls
        };
 
        ChatCompletion completion = chatResponse.AsOpenAIChatCompletion();
 
        Assert.Equal("complex-function-call", completion.Id);
        Assert.Equal(OpenAI.Chat.ChatFinishReason.ToolCalls, completion.FinishReason);
 
        var toolCall = Assert.Single(completion.ToolCalls);
        Assert.Equal("process_data", toolCall.Id);
        Assert.Equal("ProcessComplexData", toolCall.FunctionName);
 
        var deserializedArgs = JsonSerializer.Deserialize<Dictionary<string, object?>>(toolCall.FunctionArguments.ToMemory().Span);
        Assert.NotNull(deserializedArgs);
        Assert.Equal("hello", deserializedArgs["simpleString"]?.ToString());
        Assert.Equal(42, ((JsonElement)deserializedArgs["number"]!).GetInt32());
        Assert.True(((JsonElement)deserializedArgs["boolean"]!).GetBoolean());
        Assert.Null(deserializedArgs["nullValue"]);
 
        var nestedObj = (JsonElement)deserializedArgs["nestedObject"]!;
        Assert.Equal("world", nestedObj.GetProperty("innerString").GetString());
        Assert.Equal(3, nestedObj.GetProperty("innerArray").GetArrayLength());
    }
 
    [Fact]
    public void AsOpenAIChatCompletion_WithDifferentFinishReasons_MapsCorrectly()
    {
        var testCases = new[]
        {
            (ChatFinishReason.Stop, OpenAI.Chat.ChatFinishReason.Stop),
            (ChatFinishReason.Length, OpenAI.Chat.ChatFinishReason.Length),
            (ChatFinishReason.ContentFilter, OpenAI.Chat.ChatFinishReason.ContentFilter),
            (ChatFinishReason.ToolCalls, OpenAI.Chat.ChatFinishReason.ToolCalls)
        };
 
        foreach (var (inputFinishReason, expectedOpenAIFinishReason) in testCases)
        {
            var chatResponse = new ChatResponse(new ChatMessage(ChatRole.Assistant, "Test"))
            {
                FinishReason = inputFinishReason
            };
 
            var completion = chatResponse.AsOpenAIChatCompletion();
            Assert.Equal(expectedOpenAIFinishReason, completion.FinishReason);
        }
    }
 
    [Fact]
    public void AsOpenAIChatCompletion_WithDifferentRoles_MapsCorrectly()
    {
        var testCases = new[]
        {
            (ChatRole.Assistant, ChatMessageRole.Assistant),
            (ChatRole.User, ChatMessageRole.User),
            (ChatRole.System, ChatMessageRole.System),
            (ChatRole.Tool, ChatMessageRole.Tool)
        };
 
        foreach (var (inputRole, expectedOpenAIRole) in testCases)
        {
            var chatResponse = new ChatResponse(new ChatMessage(inputRole, "Test"));
            var completion = chatResponse.AsOpenAIChatCompletion();
            Assert.Equal(expectedOpenAIRole, completion.Role);
        }
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithNullArgument_ThrowsArgumentNullException()
    {
        var asyncEnumerable = ((IAsyncEnumerable<ChatResponseUpdate>)null!).AsOpenAIStreamingChatCompletionUpdatesAsync();
        await Assert.ThrowsAsync<ArgumentNullException>(async () => await asyncEnumerable.GetAsyncEnumerator().MoveNextAsync());
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithEmptyCollection_ReturnsEmptySequence()
    {
        var updates = new List<ChatResponseUpdate>();
        var result = new List<StreamingChatCompletionUpdate>();
 
        await foreach (var update in CreateAsyncEnumerable(updates).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Empty(result);
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithRawRepresentation_ReturnsOriginal()
    {
        var originalUpdate = OpenAIChatModelFactory.StreamingChatCompletionUpdate(
            "test-id",
            new ChatMessageContent(ChatMessageContentPart.CreateTextPart("Hello")),
            role: ChatMessageRole.Assistant,
            finishReason: OpenAI.Chat.ChatFinishReason.Stop,
            createdAt: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
            model: "gpt-3.5-turbo");
 
        var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, "Hello")
        {
            RawRepresentation = originalUpdate
        };
 
        var result = new List<StreamingChatCompletionUpdate>();
        await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Single(result);
        Assert.Same(originalUpdate, result[0]);
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithTextContent_CreatesValidUpdate()
    {
        var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, "Hello, world!")
        {
            ResponseId = "response-123",
            MessageId = "message-456",
            ModelId = "gpt-4",
            FinishReason = ChatFinishReason.Stop,
            CreatedAt = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero)
        };
 
        var result = new List<StreamingChatCompletionUpdate>();
        await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Single(result);
        var streamingUpdate = result[0];
 
        Assert.Equal("response-123", streamingUpdate.CompletionId);
        Assert.Equal("gpt-4", streamingUpdate.Model);
        Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, streamingUpdate.FinishReason);
        Assert.Equal(new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero), streamingUpdate.CreatedAt);
        Assert.Equal(ChatMessageRole.Assistant, streamingUpdate.Role);
        Assert.Equal("Hello, world!", Assert.Single(streamingUpdate.ContentUpdate).Text);
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithUsageContent_CreatesUpdateWithUsage()
    {
        var responseUpdate = new ChatResponseUpdate
        {
            ResponseId = "response-123",
            Contents =
            [
                new UsageContent(new UsageDetails
                {
                    InputTokenCount = 10,
                    OutputTokenCount = 20,
                    TotalTokenCount = 30
                })
            ]
        };
 
        var result = new List<StreamingChatCompletionUpdate>();
        await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Single(result);
        var streamingUpdate = result[0];
 
        Assert.Equal("response-123", streamingUpdate.CompletionId);
        Assert.NotNull(streamingUpdate.Usage);
        Assert.Equal(20, streamingUpdate.Usage.OutputTokenCount);
        Assert.Equal(10, streamingUpdate.Usage.InputTokenCount);
        Assert.Equal(30, streamingUpdate.Usage.TotalTokenCount);
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithFunctionCallContent_CreatesUpdateWithToolCalls()
    {
        var functionCallContent = new FunctionCallContent("call-123", "GetWeather", new Dictionary<string, object?>
        {
            ["location"] = "Seattle",
            ["units"] = "celsius"
        });
 
        var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, [functionCallContent])
        {
            ResponseId = "response-123"
        };
 
        var result = new List<StreamingChatCompletionUpdate>();
        await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Single(result);
        var streamingUpdate = result[0];
 
        Assert.Equal("response-123", streamingUpdate.CompletionId);
        Assert.Single(streamingUpdate.ToolCallUpdates);
 
        var toolCallUpdate = streamingUpdate.ToolCallUpdates[0];
        Assert.Equal(0, toolCallUpdate.Index);
        Assert.Equal("call-123", toolCallUpdate.ToolCallId);
        Assert.Equal(ChatToolCallKind.Function, toolCallUpdate.Kind);
        Assert.Equal("GetWeather", toolCallUpdate.FunctionName);
 
        var deserializedArgs = JsonSerializer.Deserialize<Dictionary<string, object?>>(
            toolCallUpdate.FunctionArgumentsUpdate.ToMemory().Span);
        Assert.Equal("Seattle", deserializedArgs?["location"]?.ToString());
        Assert.Equal("celsius", deserializedArgs?["units"]?.ToString());
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMultipleFunctionCalls_CreatesCorrectIndexes()
    {
        var functionCall1 = new FunctionCallContent("call-1", "Function1", new Dictionary<string, object?> { ["param1"] = "value1" });
        var functionCall2 = new FunctionCallContent("call-2", "Function2", new Dictionary<string, object?> { ["param2"] = "value2" });
 
        var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, [functionCall1, functionCall2])
        {
            ResponseId = "response-123"
        };
 
        var result = new List<StreamingChatCompletionUpdate>();
        await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Single(result);
        var streamingUpdate = result[0];
 
        Assert.Equal(2, streamingUpdate.ToolCallUpdates.Count);
 
        Assert.Equal(0, streamingUpdate.ToolCallUpdates[0].Index);
        Assert.Equal("call-1", streamingUpdate.ToolCallUpdates[0].ToolCallId);
        Assert.Equal("Function1", streamingUpdate.ToolCallUpdates[0].FunctionName);
 
        Assert.Equal(1, streamingUpdate.ToolCallUpdates[1].Index);
        Assert.Equal("call-2", streamingUpdate.ToolCallUpdates[1].ToolCallId);
        Assert.Equal("Function2", streamingUpdate.ToolCallUpdates[1].FunctionName);
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMixedContent_IncludesAllContent()
    {
        var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant,
        [
            new TextContent("Processing your request..."),
            new FunctionCallContent("call-123", "GetWeather", new Dictionary<string, object?> { ["location"] = "Seattle" }),
            new UsageContent(new UsageDetails { TotalTokenCount = 50 })
        ])
        {
            ResponseId = "response-123",
            ModelId = "gpt-4"
        };
 
        var result = new List<StreamingChatCompletionUpdate>();
        await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Single(result);
        var streamingUpdate = result[0];
 
        Assert.Equal("response-123", streamingUpdate.CompletionId);
        Assert.Equal("gpt-4", streamingUpdate.Model);
 
        // Should have text content
        Assert.Contains(streamingUpdate.ContentUpdate, c => c.Text == "Processing your request...");
 
        // Should have tool call
        Assert.Single(streamingUpdate.ToolCallUpdates);
        Assert.Equal("call-123", streamingUpdate.ToolCallUpdates[0].ToolCallId);
 
        // Should have usage
        Assert.NotNull(streamingUpdate.Usage);
        Assert.Equal(50, streamingUpdate.Usage.TotalTokenCount);
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithDifferentRoles_MapsCorrectly()
    {
        var testCases = new[]
        {
            (ChatRole.Assistant, ChatMessageRole.Assistant),
            (ChatRole.User, ChatMessageRole.User),
            (ChatRole.System, ChatMessageRole.System),
            (ChatRole.Tool, ChatMessageRole.Tool)
        };
 
        foreach (var (inputRole, expectedOpenAIRole) in testCases)
        {
            var responseUpdate = new ChatResponseUpdate(inputRole, "Test message");
 
            var result = new List<StreamingChatCompletionUpdate>();
            await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
            {
                result.Add(update);
            }
 
            Assert.Single(result);
            Assert.Equal(expectedOpenAIRole, result[0].Role);
        }
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithDifferentFinishReasons_MapsCorrectly()
    {
        var testCases = new[]
        {
            (ChatFinishReason.Stop, OpenAI.Chat.ChatFinishReason.Stop),
            (ChatFinishReason.Length, OpenAI.Chat.ChatFinishReason.Length),
            (ChatFinishReason.ContentFilter, OpenAI.Chat.ChatFinishReason.ContentFilter),
            (ChatFinishReason.ToolCalls, OpenAI.Chat.ChatFinishReason.ToolCalls)
        };
 
        foreach (var (inputFinishReason, expectedOpenAIFinishReason) in testCases)
        {
            var responseUpdate = new ChatResponseUpdate(ChatRole.Assistant, "Test")
            {
                FinishReason = inputFinishReason
            };
 
            var result = new List<StreamingChatCompletionUpdate>();
            await foreach (var update in CreateAsyncEnumerable(new[] { responseUpdate }).AsOpenAIStreamingChatCompletionUpdatesAsync())
            {
                result.Add(update);
            }
 
            Assert.Single(result);
            Assert.Equal(expectedOpenAIFinishReason, result[0].FinishReason);
        }
    }
 
    [Fact]
    public async Task AsOpenAIStreamingChatCompletionUpdatesAsync_WithMultipleUpdates_ProcessesAllCorrectly()
    {
        var updates = new[]
        {
            new ChatResponseUpdate(ChatRole.Assistant, "Hello, ")
            {
                ResponseId = "response-123",
                MessageId = "message-1"
 
                // No FinishReason set - null
            },
            new ChatResponseUpdate(ChatRole.Assistant, "world!")
            {
                ResponseId = "response-123",
                MessageId = "message-1",
                FinishReason = ChatFinishReason.Stop
            }
        };
 
        var result = new List<StreamingChatCompletionUpdate>();
        await foreach (var update in CreateAsyncEnumerable(updates).AsOpenAIStreamingChatCompletionUpdatesAsync())
        {
            result.Add(update);
        }
 
        Assert.Equal(2, result.Count);
 
        Assert.Equal("response-123", result[0].CompletionId);
        Assert.Equal("Hello, ", Assert.Single(result[0].ContentUpdate).Text);
 
        // The ToChatFinishReason method defaults null to Stop
        Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, result[0].FinishReason);
 
        Assert.Equal("response-123", result[1].CompletionId);
        Assert.Equal("world!", Assert.Single(result[1].ContentUpdate).Text);
        Assert.Equal(OpenAI.Chat.ChatFinishReason.Stop, result[1].FinishReason);
    }
 
    private static async IAsyncEnumerable<T> CreateAsyncEnumerable<T>(IEnumerable<T> source)
    {
        foreach (var item in source)
        {
            await Task.Yield();
            yield return item;
        }
    }
}