|
// 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.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using OpenAI.Chat;
using Xunit;
#pragma warning disable S103 // Lines should not be too long
namespace Microsoft.Extensions.AI;
public static partial class OpenAISerializationTests
{
[Fact]
public static async Task RequestDeserialization_SimpleMessage()
{
const string RequestJson = """
{"messages":[{"role":"user","content":"hello"}],"model":"gpt-4o-mini","max_completion_tokens":10,"temperature":0.5}
"""{"messages":[{"role":"user","content":"hello"}],"model":"gpt-4o-mini","max_completion_tokens":10,"temperature":0.5}
""";
using MemoryStream stream = new(Encoding.UTF8.GetBytes(RequestJson));
OpenAIChatCompletionRequest request = await OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(stream);
Assert.NotNull(request);
Assert.False(request.Stream);
Assert.Equal("gpt-4o-mini", request.ModelId);
Assert.NotNull(request.Options);
Assert.Equal("gpt-4o-mini", request.Options.ModelId);
Assert.Equal(0.5f, request.Options.Temperature);
Assert.Equal(10, request.Options.MaxOutputTokens);
Assert.Null(request.Options.TopK);
Assert.Null(request.Options.TopP);
Assert.Null(request.Options.StopSequences);
Assert.Null(request.Options.AdditionalProperties);
Assert.Null(request.Options.Tools);
ChatMessage message = Assert.Single(request.Messages);
Assert.Equal(ChatRole.User, message.Role);
AIContent content = Assert.Single(message.Contents);
TextContent textContent = Assert.IsType<TextContent>(content);
Assert.Equal("hello", textContent.Text);
Assert.Null(textContent.RawRepresentation);
Assert.Null(textContent.AdditionalProperties);
}
[Fact]
public static async Task RequestDeserialization_SimpleMessage_Stream()
{
const string RequestJson = """
{"messages":[{"role":"user","content":"hello"}],"model":"gpt-4o-mini","max_completion_tokens":20,"stream":true,"stream_options":{"include_usage":true},"temperature":0.5}
"""{"messages":[{"role":"user","content":"hello"}],"model":"gpt-4o-mini","max_completion_tokens":20,"stream":true,"stream_options":{"include_usage":true},"temperature":0.5}
""";
using MemoryStream stream = new(Encoding.UTF8.GetBytes(RequestJson));
OpenAIChatCompletionRequest request = await OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(stream);
Assert.NotNull(request);
Assert.True(request.Stream);
Assert.Equal("gpt-4o-mini", request.ModelId);
Assert.NotNull(request.Options);
Assert.Equal("gpt-4o-mini", request.Options.ModelId);
Assert.Equal(0.5f, request.Options.Temperature);
Assert.Equal(20, request.Options.MaxOutputTokens);
Assert.Null(request.Options.TopK);
Assert.Null(request.Options.TopP);
Assert.Null(request.Options.StopSequences);
Assert.Null(request.Options.AdditionalProperties);
Assert.Null(request.Options.Tools);
ChatMessage message = Assert.Single(request.Messages);
Assert.Equal(ChatRole.User, message.Role);
AIContent content = Assert.Single(message.Contents);
TextContent textContent = Assert.IsType<TextContent>(content);
Assert.Equal("hello", textContent.Text);
Assert.Null(textContent.RawRepresentation);
Assert.Null(textContent.AdditionalProperties);
}
[Fact]
public static void RequestDeserialization_SimpleMessage_JsonSerializer()
{
const string RequestJson = """
{"messages":[{"role":"user","content":"hello"}],"model":"gpt-4o-mini","max_completion_tokens":20,"stream":true,"stream_options":{"include_usage":true},"temperature":0.5}
"""{"messages":[{"role":"user","content":"hello"}],"model":"gpt-4o-mini","max_completion_tokens":20,"stream":true,"stream_options":{"include_usage":true},"temperature":0.5}
""";
OpenAIChatCompletionRequest? request = JsonSerializer.Deserialize<OpenAIChatCompletionRequest>(RequestJson);
Assert.NotNull(request);
Assert.True(request.Stream);
Assert.Equal("gpt-4o-mini", request.ModelId);
Assert.NotNull(request.Options);
Assert.Equal("gpt-4o-mini", request.Options.ModelId);
Assert.Equal(0.5f, request.Options.Temperature);
Assert.Equal(20, request.Options.MaxOutputTokens);
Assert.Null(request.Options.TopK);
Assert.Null(request.Options.TopP);
Assert.Null(request.Options.StopSequences);
Assert.Null(request.Options.AdditionalProperties);
Assert.Null(request.Options.Tools);
ChatMessage message = Assert.Single(request.Messages);
Assert.Equal(ChatRole.User, message.Role);
AIContent content = Assert.Single(message.Contents);
TextContent textContent = Assert.IsType<TextContent>(content);
Assert.Equal("hello", textContent.Text);
Assert.Null(textContent.RawRepresentation);
Assert.Null(textContent.AdditionalProperties);
}
[Fact]
public static async Task RequestDeserialization_MultipleMessages()
{
const string RequestJson = """
{
"messages": [
{
"role": "system",
"content": "You are a really nice friend."
},
{
"role": "user",
"content": "hello!"
},
{
"role": "assistant",
"content": "hi, how are you?"
},
{
"role": "user",
"content": "i\u0027m good. how are you?"
}
],
"model": "gpt-4o-mini",
"frequency_penalty": 0.75,
"presence_penalty": 0.5,
"seed":42,
"stop": [ "great" ],
"temperature": 0.25,
"user": "user",
"logprobs": true,
"logit_bias": { "42" : 0 },
"parallel_tool_calls": true,
"top_logprobs": 42,
"metadata": { "key": "value" },
"store": true
}
"""{
"messages": [
{
"role": "system",
"content": "You are a really nice friend."
},
{
"role": "user",
"content": "hello!"
},
{
"role": "assistant",
"content": "hi, how are you?"
},
{
"role": "user",
"content": "i\u0027m good. how are you?"
}
],
"model": "gpt-4o-mini",
"frequency_penalty": 0.75,
"presence_penalty": 0.5,
"seed":42,
"stop": [ "great" ],
"temperature": 0.25,
"user": "user",
"logprobs": true,
"logit_bias": { "42" : 0 },
"parallel_tool_calls": true,
"top_logprobs": 42,
"metadata": { "key": "value" },
"store": true
}
""";
using MemoryStream stream = new(Encoding.UTF8.GetBytes(RequestJson));
OpenAIChatCompletionRequest request = await OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(stream);
Assert.NotNull(request);
Assert.False(request.Stream);
Assert.Equal("gpt-4o-mini", request.ModelId);
Assert.NotNull(request.Options);
Assert.Equal("gpt-4o-mini", request.Options.ModelId);
Assert.Equal(0.25f, request.Options.Temperature);
Assert.Equal(0.75f, request.Options.FrequencyPenalty);
Assert.Equal(0.5f, request.Options.PresencePenalty);
Assert.Equal(42, request.Options.Seed);
Assert.Equal(["great"], request.Options.StopSequences);
Assert.NotNull(request.Options.AdditionalProperties);
Assert.Equal("user", request.Options.AdditionalProperties["EndUserId"]);
Assert.True((bool)request.Options.AdditionalProperties["IncludeLogProbabilities"]!);
Assert.Single((IDictionary<int, int>)request.Options.AdditionalProperties["LogitBiases"]!);
Assert.True((bool)request.Options.AdditionalProperties["AllowParallelToolCalls"]!);
Assert.Equal(42, request.Options.AdditionalProperties["TopLogProbabilityCount"]!);
Assert.Single((IDictionary<string, string>)request.Options.AdditionalProperties["Metadata"]!);
Assert.True((bool)request.Options.AdditionalProperties["StoredOutputEnabled"]!);
Assert.Collection(request.Messages,
msg =>
{
Assert.Equal(ChatRole.System, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
TextContent text = Assert.IsType<TextContent>(Assert.Single(msg.Contents));
Assert.Equal("You are a really nice friend.", text.Text);
Assert.Null(text.AdditionalProperties);
Assert.Null(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
},
msg =>
{
Assert.Equal(ChatRole.User, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
TextContent text = Assert.IsType<TextContent>(Assert.Single(msg.Contents));
Assert.Equal("hello!", text.Text);
Assert.Null(text.AdditionalProperties);
Assert.Null(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
},
msg =>
{
Assert.Equal(ChatRole.Assistant, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
TextContent text = Assert.IsType<TextContent>(Assert.Single(msg.Contents));
Assert.Equal("hi, how are you?", text.Text);
Assert.Null(text.AdditionalProperties);
Assert.Null(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
},
msg =>
{
Assert.Equal(ChatRole.User, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
TextContent text = Assert.IsType<TextContent>(Assert.Single(msg.Contents));
Assert.Equal("i'm good. how are you?", text.Text);
Assert.Null(text.AdditionalProperties);
Assert.Null(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
});
}
[Fact]
public static async Task RequestDeserialization_MultiPartSystemMessage()
{
const string RequestJson = """
{
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a really nice friend."
},
{
"type": "text",
"text": "Really nice."
}
]
},
{
"role": "user",
"content": "hello!"
}
],
"model": "gpt-4o-mini"
}
"""{
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a really nice friend."
},
{
"type": "text",
"text": "Really nice."
}
]
},
{
"role": "user",
"content": "hello!"
}
],
"model": "gpt-4o-mini"
}
""";
using MemoryStream stream = new(Encoding.UTF8.GetBytes(RequestJson));
OpenAIChatCompletionRequest request = await OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(stream);
Assert.NotNull(request);
Assert.False(request.Stream);
Assert.Equal("gpt-4o-mini", request.ModelId);
Assert.NotNull(request.Options);
Assert.Equal("gpt-4o-mini", request.Options.ModelId);
Assert.Null(request.Options.Temperature);
Assert.Null(request.Options.FrequencyPenalty);
Assert.Null(request.Options.PresencePenalty);
Assert.Null(request.Options.Seed);
Assert.Null(request.Options.StopSequences);
Assert.Collection(request.Messages,
msg =>
{
Assert.Equal(ChatRole.System, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
Assert.Collection(msg.Contents,
content =>
{
TextContent text = Assert.IsType<TextContent>(content);
Assert.Equal("You are a really nice friend.", text.Text);
Assert.Null(text.AdditionalProperties);
Assert.Null(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
},
content =>
{
TextContent text = Assert.IsType<TextContent>(content);
Assert.Equal("Really nice.", text.Text);
Assert.Null(text.AdditionalProperties);
Assert.Null(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
});
},
msg =>
{
Assert.Equal(ChatRole.User, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
TextContent text = Assert.IsType<TextContent>(Assert.Single(msg.Contents));
Assert.Equal("hello!", text.Text);
Assert.Null(text.AdditionalProperties);
Assert.Null(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
});
}
[Fact]
public static async Task RequestDeserialization_ToolCall()
{
const string RequestJson = """
{
"messages": [
{
"role": "user",
"content": "How old is Alice?"
}
],
"model": "gpt-4o-mini",
"tools": [
{
"type": "function",
"function": {
"description": "Gets the age of the specified person.",
"name": "GetPersonAge",
"strict": true,
"parameters": {
"type": "object",
"required": [
"personName"
],
"properties": {
"personName": {
"description": "The person whose age is being requested",
"type": "string"
}
}
}
}
}
],
"tool_choice": "auto"
}
"""{
"messages": [
{
"role": "user",
"content": "How old is Alice?"
}
],
"model": "gpt-4o-mini",
"tools": [
{
"type": "function",
"function": {
"description": "Gets the age of the specified person.",
"name": "GetPersonAge",
"strict": true,
"parameters": {
"type": "object",
"required": [
"personName"
],
"properties": {
"personName": {
"description": "The person whose age is being requested",
"type": "string"
}
}
}
}
}
],
"tool_choice": "auto"
}
""";
using MemoryStream stream = new(Encoding.UTF8.GetBytes(RequestJson));
OpenAIChatCompletionRequest request = await OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(stream);
Assert.NotNull(request);
Assert.False(request.Stream);
Assert.Equal("gpt-4o-mini", request.ModelId);
Assert.NotNull(request.Options);
Assert.Equal("gpt-4o-mini", request.Options.ModelId);
Assert.Null(request.Options.Temperature);
Assert.Null(request.Options.FrequencyPenalty);
Assert.Null(request.Options.PresencePenalty);
Assert.Null(request.Options.Seed);
Assert.Null(request.Options.StopSequences);
Assert.Equal(ChatToolMode.Auto, request.Options.ToolMode);
Assert.NotNull(request.Options.Tools);
AIFunction function = Assert.IsAssignableFrom<AIFunction>(Assert.Single(request.Options.Tools));
Assert.Equal("Gets the age of the specified person.", function.Metadata.Description);
Assert.Equal("GetPersonAge", function.Metadata.Name);
Assert.Equal("Strict", Assert.Single(function.Metadata.AdditionalProperties).Key);
Assert.Equal("Return parameter", function.Metadata.ReturnParameter.Description);
Assert.Equal("{}", Assert.IsType<JsonElement>(function.Metadata.ReturnParameter.Schema).GetRawText());
AIFunctionParameterMetadata parameter = Assert.Single(function.Metadata.Parameters);
Assert.Equal("personName", parameter.Name);
Assert.True(parameter.IsRequired);
JsonObject parameterSchema = Assert.IsType<JsonObject>(JsonNode.Parse(Assert.IsType<JsonElement>(parameter.Schema).GetRawText()));
Assert.Equal(2, parameterSchema.Count);
Assert.Equal("The person whose age is being requested", (string)parameterSchema["description"]!);
Assert.Equal("string", (string)parameterSchema["type"]!);
Dictionary<string, object?> functionArgs = new() { ["personName"] = "John" };
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => function.InvokeAsync(functionArgs));
Assert.Contains("does not support being invoked.", ex.Message);
}
[Fact]
public static async Task RequestDeserialization_ToolChatMessage()
{
const string RequestJson = """
{
"messages": [
{
"role": "assistant",
"tool_calls": [
{
"id": "12345",
"type": "function",
"function": {
"name": "SayHello",
"arguments": "null"
}
}
]
},
{
"role": "tool",
"tool_call_id": "12345",
"content": "42"
}
],
"model": "gpt-4o-mini"
}
"""{
"messages": [
{
"role": "assistant",
"tool_calls": [
{
"id": "12345",
"type": "function",
"function": {
"name": "SayHello",
"arguments": "null"
}
}
]
},
{
"role": "tool",
"tool_call_id": "12345",
"content": "42"
}
],
"model": "gpt-4o-mini"
}
""";
using MemoryStream stream = new(Encoding.UTF8.GetBytes(RequestJson));
OpenAIChatCompletionRequest request = await OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(stream);
Assert.NotNull(request);
Assert.False(request.Stream);
Assert.Equal("gpt-4o-mini", request.ModelId);
Assert.NotNull(request.Options);
Assert.Equal("gpt-4o-mini", request.Options.ModelId);
Assert.Null(request.Options.Temperature);
Assert.Null(request.Options.FrequencyPenalty);
Assert.Null(request.Options.PresencePenalty);
Assert.Null(request.Options.Seed);
Assert.Null(request.Options.StopSequences);
Assert.Collection(request.Messages,
msg =>
{
Assert.Equal(ChatRole.Assistant, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
FunctionCallContent text = Assert.IsType<FunctionCallContent>(Assert.Single(msg.Contents));
Assert.Equal("12345", text.CallId);
Assert.Null(text.AdditionalProperties);
Assert.IsType<OpenAI.Chat.ChatToolCall>(text.RawRepresentation);
Assert.Null(text.AdditionalProperties);
},
msg =>
{
Assert.Equal(ChatRole.Tool, msg.Role);
Assert.Null(msg.RawRepresentation);
Assert.Null(msg.AdditionalProperties);
FunctionResultContent frc = Assert.IsType<FunctionResultContent>(Assert.Single(msg.Contents));
Assert.Equal("SayHello", frc.Name);
Assert.Equal("12345", frc.CallId);
Assert.Equal(42, Assert.IsType<JsonElement>(frc.Result).GetInt32());
Assert.Null(frc.AdditionalProperties);
Assert.Null(frc.RawRepresentation);
Assert.Null(frc.AdditionalProperties);
});
}
[Fact]
public static async Task SerializeCompletion_SingleChoice()
{
ChatMessage message = new()
{
Role = ChatRole.Assistant,
Contents = [
new TextContent("Hello! How can I assist you today?"),
new FunctionCallContent(
"callId",
"MyCoolFunc",
new Dictionary<string, object?>
{
["arg1"] = 42,
["arg2"] = "str",
})
]
};
ChatCompletion completion = new(message)
{
CompletionId = "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI",
ModelId = "gpt-4o-mini-2024-07-18",
CreatedAt = DateTimeOffset.FromUnixTimeSeconds(1_727_888_631),
FinishReason = ChatFinishReason.Stop,
Usage = new()
{
InputTokenCount = 8,
OutputTokenCount = 9,
TotalTokenCount = 17,
AdditionalCounts = new()
{
{ "InputTokenDetails.AudioTokenCount", 1 },
{ "InputTokenDetails.CachedTokenCount", 13 },
{ "OutputTokenDetails.AudioTokenCount", 2 },
{ "OutputTokenDetails.ReasoningTokenCount", 90 },
}
},
AdditionalProperties = new()
{
[nameof(OpenAI.Chat.ChatCompletion.SystemFingerprint)] = "fp_f85bea6784",
}
};
using MemoryStream stream = new();
await OpenAISerializationHelpers.SerializeAsync(stream, completion);
string result = Encoding.UTF8.GetString(stream.ToArray());
AssertJsonEqual("""
{
"id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI",
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "Hello! How can I assist you today?",
"refusal": null,
"tool_calls": [
{
"id": "callId",
"type": "function",
"function": {
"name": "MyCoolFunc",
"arguments": "{\r\n \u0022arg1\u0022: 42,\r\n \u0022arg2\u0022: \u0022str\u0022\r\n}"
}
}
],
"role": "assistant"
},
"logprobs": {
"content": [],
"refusal": []
}
}
],
"created": 1727888631,
"model": "gpt-4o-mini-2024-07-18",
"system_fingerprint": "fp_f85bea6784",
"object": "chat.completion",
"usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
"completion_tokens_details": {
"audio_tokens": 2,
"reasoning_tokens": 90
},
"prompt_tokens_details": {
"audio_tokens": 1,
"cached_tokens": 13
}
}
}
"""{
"id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI",
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "Hello! How can I assist you today?",
"refusal": null,
"tool_calls": [
{
"id": "callId",
"type": "function",
"function": {
"name": "MyCoolFunc",
"arguments": "{\r\n \u0022arg1\u0022: 42,\r\n \u0022arg2\u0022: \u0022str\u0022\r\n}"
}
}
],
"role": "assistant"
},
"logprobs": {
"content": [],
"refusal": []
}
}
],
"created": 1727888631,
"model": "gpt-4o-mini-2024-07-18",
"system_fingerprint": "fp_f85bea6784",
"object": "chat.completion",
"usage": {
"completion_tokens": 9,
"prompt_tokens": 8,
"total_tokens": 17,
"completion_tokens_details": {
"audio_tokens": 2,
"reasoning_tokens": 90
},
"prompt_tokens_details": {
"audio_tokens": 1,
"cached_tokens": 13
}
}
}
""", result);
}
[Fact]
public static async Task SerializeCompletion_ManyChoices_ThrowsNotSupportedException()
{
ChatMessage message1 = new()
{
Role = ChatRole.Assistant,
Text = "Hello! How can I assist you today?",
};
ChatMessage message2 = new()
{
Role = ChatRole.Assistant,
Text = "Hey there! How can I help?",
};
ChatCompletion completion = new([message1, message2]);
using MemoryStream stream = new();
var ex = await Assert.ThrowsAsync<NotSupportedException>(() => OpenAISerializationHelpers.SerializeAsync(stream, completion));
Assert.Contains("multiple choices", ex.Message);
}
[Fact]
public static async Task SerializeStreamingCompletion()
{
static async IAsyncEnumerable<StreamingChatCompletionUpdate> CreateStreamingCompletion()
{
for (int i = 0; i < 5; i++)
{
List<AIContent> contents = [new TextContent($"Streaming update {i}")];
if (i == 2)
{
FunctionCallContent fcc = new(
"callId",
"MyCoolFunc",
new Dictionary<string, object?>
{
["arg1"] = 42,
["arg2"] = "str",
});
contents.Add(fcc);
}
if (i == 4)
{
UsageDetails usageDetails = new()
{
InputTokenCount = 8,
OutputTokenCount = 9,
TotalTokenCount = 17,
AdditionalCounts = new()
{
{ "InputTokenDetails.AudioTokenCount", 1 },
{ "InputTokenDetails.CachedTokenCount", 13 },
{ "OutputTokenDetails.AudioTokenCount", 2 },
{ "OutputTokenDetails.ReasoningTokenCount", 90 },
}
};
contents.Add(new UsageContent(usageDetails));
}
yield return new StreamingChatCompletionUpdate
{
CompletionId = "chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl",
ModelId = "gpt-4o-mini-2024-07-18",
CreatedAt = DateTimeOffset.FromUnixTimeSeconds(1_727_888_631),
Role = ChatRole.Assistant,
Contents = contents,
FinishReason = i == 4 ? ChatFinishReason.Stop : null,
AdditionalProperties = new()
{
[nameof(OpenAI.Chat.ChatCompletion.SystemFingerprint)] = "fp_f85bea6784",
},
};
await Task.Yield();
}
}
using MemoryStream stream = new();
await OpenAISerializationHelpers.SerializeStreamingAsync(stream, CreateStreamingCompletion());
string result = Encoding.UTF8.GetString(stream.ToArray());
AssertSseEqual("""
data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","choices":[{"delta":{"content":"Streaming update 0","tool_calls":[],"role":"assistant"},"logprobs":{"content":[],"refusal":[]},"index":0}],"created":1727888631,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","object":"chat.completion.chunk"}
data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","choices":[{"delta":{"content":"Streaming update 1","tool_calls":[],"role":"assistant"},"logprobs":{"content":[],"refusal":[]},"index":0}],"created":1727888631,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","object":"chat.completion.chunk"}
data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","choices":[{"delta":{"content":"Streaming update 2","tool_calls":[{"index":0,"id":"callId","type":"function","function":{"name":"MyCoolFunc","arguments":"{\r\n \u0022arg1\u0022: 42,\r\n \u0022arg2\u0022: \u0022str\u0022\r\n}"}}],"role":"assistant"},"logprobs":{"content":[],"refusal":[]},"index":0}],"created":1727888631,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","object":"chat.completion.chunk"}
data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","choices":[{"delta":{"content":"Streaming update 3","tool_calls":[],"role":"assistant"},"logprobs":{"content":[],"refusal":[]},"index":0}],"created":1727888631,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","object":"chat.completion.chunk"}
data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","choices":[{"delta":{"content":"Streaming update 4","tool_calls":[],"role":"assistant"},"logprobs":{"content":[],"refusal":[]},"finish_reason":"stop","index":0}],"created":1727888631,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","object":"chat.completion.chunk","usage":{"completion_tokens":9,"prompt_tokens":8,"total_tokens":17,"completion_tokens_details":{"audio_tokens":2,"reasoning_tokens":90},"prompt_tokens_details":{"audio_tokens":1,"cached_tokens":13}}}
data: [DONE]
""", result);
}
[Fact]
public static async Task SerializationHelpers_NullArguments_ThrowsArgumentNullException()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(null!));
await Assert.ThrowsAsync<ArgumentNullException>(() => OpenAISerializationHelpers.SerializeAsync(null!, new(new ChatMessage())));
await Assert.ThrowsAsync<ArgumentNullException>(() => OpenAISerializationHelpers.SerializeAsync(new MemoryStream(), null!));
await Assert.ThrowsAsync<ArgumentNullException>(() => OpenAISerializationHelpers.SerializeStreamingAsync(null!, GetStreamingChatCompletion()));
await Assert.ThrowsAsync<ArgumentNullException>(() => OpenAISerializationHelpers.SerializeStreamingAsync(new MemoryStream(), null!));
static async IAsyncEnumerable<StreamingChatCompletionUpdate> GetStreamingChatCompletion()
{
yield return new StreamingChatCompletionUpdate();
await Task.CompletedTask;
}
}
[Fact]
public static async Task SerializationHelpers_HonorCancellationToken()
{
CancellationToken canceledToken = new(canceled: true);
MemoryStream stream = new("{}"u8.ToArray());
await Assert.ThrowsAsync<TaskCanceledException>(() => OpenAISerializationHelpers.DeserializeChatCompletionRequestAsync(stream, cancellationToken: canceledToken));
await Assert.ThrowsAsync<TaskCanceledException>(() => OpenAISerializationHelpers.SerializeAsync(stream, new(new ChatMessage()), cancellationToken: canceledToken));
await Assert.ThrowsAsync<TaskCanceledException>(() => OpenAISerializationHelpers.SerializeStreamingAsync(stream, GetStreamingChatCompletion(), cancellationToken: canceledToken));
static async IAsyncEnumerable<StreamingChatCompletionUpdate> GetStreamingChatCompletion()
{
yield return new StreamingChatCompletionUpdate();
await Task.CompletedTask;
}
}
[Fact]
public static async Task SerializationHelpers_HonorJsonSerializerOptions()
{
FunctionCallContent fcc = new(
"callId",
"MyCoolFunc",
new Dictionary<string, object?>
{
["arg1"] = new SomeFunctionArgument(),
});
ChatCompletion completion = new(new ChatMessage
{
Role = ChatRole.Assistant,
Contents = [fcc],
});
using MemoryStream stream = new();
// Passing a JSO that contains a contract for the function argument results in successful serialization.
await OpenAISerializationHelpers.SerializeAsync(stream, completion, options: JsonContextWithFunctionArgument.Default.Options);
stream.Position = 0;
await OpenAISerializationHelpers.SerializeStreamingAsync(stream, GetStreamingCompletion(), options: JsonContextWithFunctionArgument.Default.Options);
stream.Position = 0;
// Passing a JSO without a contract for the function argument result in failed serialization.
await Assert.ThrowsAsync<NotSupportedException>(() => OpenAISerializationHelpers.SerializeAsync(stream, completion, options: JsonContextWithoutFunctionArgument.Default.Options));
await Assert.ThrowsAsync<NotSupportedException>(() => OpenAISerializationHelpers.SerializeStreamingAsync(stream, GetStreamingCompletion(), options: JsonContextWithoutFunctionArgument.Default.Options));
async IAsyncEnumerable<StreamingChatCompletionUpdate> GetStreamingCompletion()
{
yield return new StreamingChatCompletionUpdate
{
Contents = [fcc],
};
await Task.CompletedTask;
}
}
private class SomeFunctionArgument;
[JsonSerializable(typeof(SomeFunctionArgument))]
[JsonSerializable(typeof(IDictionary<string, object>))]
private partial class JsonContextWithFunctionArgument : JsonSerializerContext;
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(IDictionary<string, object>))]
private partial class JsonContextWithoutFunctionArgument : JsonSerializerContext;
private static void AssertJsonEqual(string expected, string actual)
{
JsonNode? expectedNode = JsonNode.Parse(expected);
JsonNode? actualNode = JsonNode.Parse(actual);
if (!JsonNode.DeepEquals(expectedNode, actualNode))
{
// JSON documents are not equal, assert on
// normal form strings for better reporting.
expected = expectedNode?.ToJsonString() ?? "null";
actual = actualNode?.ToJsonString() ?? "null";
Assert.Equal(expected.NormalizeNewLines(), actual.NormalizeNewLines());
}
}
private static void AssertSseEqual(string expected, string actual)
{
Assert.Equal(expected.NormalizeNewLines(), actual.NormalizeNewLines());
}
private static string NormalizeNewLines(this string value) =>
value.Replace("\r\n", "\n").Replace("\\r\\n", "\\n");
}
|