Skip to content

Commit

Permalink
Make a number of improvements to the OpenAI serialization helpers. (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
eiriktsarpalis authored Jan 17, 2025
1 parent 8f15b0f commit c63bc2c
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.ClientModel.Primitives;
using System.Text.Json;

namespace Microsoft.Extensions.AI;

Expand All @@ -23,6 +24,12 @@ public static TModel Deserialize<TModel>(BinaryData data)
return JsonModelDeserializationWitness<TModel>.Value.Create(data, ModelReaderWriterOptions.Json);
}

public static TModel Deserialize<TModel>(ref Utf8JsonReader reader)
where TModel : IJsonModel<TModel>, new()
{
return JsonModelDeserializationWitness<TModel>.Value.Create(ref reader, ModelReaderWriterOptions.Json);
}

private sealed class JsonModelDeserializationWitness<TModel>
where TModel : IJsonModel<TModel>, new()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// 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.Text.Json;
using System.Text.Json.Serialization;
using OpenAI.Chat;

#pragma warning disable CA1034 // Nested types should not be visible

namespace Microsoft.Extensions.AI;

/// <summary>
/// Represents an OpenAI chat completion request deserialized as Microsoft.Extension.AI models.
/// </summary>
[JsonConverter(typeof(Converter))]
public sealed class OpenAIChatCompletionRequest
{
/// <summary>
Expand All @@ -29,4 +37,33 @@ public sealed class OpenAIChatCompletionRequest
/// Gets the model id requested by the chat completion.
/// </summary>
public string? ModelId { get; init; }

/// <summary>
/// Converts an OpenAIChatCompletionRequest object to and from JSON.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class Converter : JsonConverter<OpenAIChatCompletionRequest>
{
/// <summary>
/// Reads and converts the JSON to type OpenAIChatCompletionRequest.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="typeToConvert">The type to convert.</param>
/// <param name="options">The serializer options.</param>
/// <returns>The converted OpenAIChatCompletionRequest object.</returns>
public override OpenAIChatCompletionRequest? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
ChatCompletionOptions chatCompletionOptions = JsonModelHelpers.Deserialize<ChatCompletionOptions>(ref reader);
return OpenAIModelMappers.FromOpenAIChatCompletionRequest(chatCompletionOptions);
}

/// <summary>
/// Writes the specified value as JSON.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="value">The value to write.</param>
/// <param name="options">The serializer options.</param>
public override void Write(Utf8JsonWriter writer, OpenAIChatCompletionRequest value, JsonSerializerOptions options) =>
throw new NotSupportedException("Request body serialization is not supported.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#pragma warning disable S103 // Lines should not be too long
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
#pragma warning disable S1067 // Expressions should not be too complex
#pragma warning disable S3440 // Variables should not be checked against the values they're about to be assigned

namespace Microsoft.Extensions.AI;

Expand Down Expand Up @@ -57,7 +58,7 @@ public static OpenAI.Chat.ChatCompletion ToOpenAIChatCompletion(ChatCompletion c
return OpenAIChatModelFactory.ChatCompletion(
id: chatCompletion.CompletionId ?? CreateCompletionId(),
model: chatCompletion.ModelId,
createdAt: chatCompletion.CreatedAt ?? default,
createdAt: chatCompletion.CreatedAt ?? DateTimeOffset.UtcNow,
role: ToOpenAIChatRole(chatCompletion.Message.Role).Value,
finishReason: ToOpenAIFinishReason(chatCompletion.FinishReason),
content: new(ToOpenAIChatContent(chatCompletion.Message.Contents)),
Expand Down Expand Up @@ -148,7 +149,12 @@ public static ChatOptions FromOpenAIOptions(OpenAI.Chat.ChatCompletionOptions? o

if (options is not null)
{
result.ModelId = _getModelIdAccessor.Invoke(options, null)?.ToString();
result.ModelId = _getModelIdAccessor.Invoke(options, null)?.ToString() switch
{
null or "" => null,
var modelId => modelId,
};

result.FrequencyPenalty = options.FrequencyPenalty;
result.MaxOutputTokens = options.MaxOutputTokenCount;
result.TopP = options.TopP;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ internal static partial class OpenAIModelMappers
yield return OpenAIChatModelFactory.StreamingChatCompletionUpdate(
completionId: chatCompletionUpdate.CompletionId ?? CreateCompletionId(),
model: chatCompletionUpdate.ModelId,
createdAt: chatCompletionUpdate.CreatedAt ?? default,
createdAt: chatCompletionUpdate.CreatedAt ?? DateTimeOffset.UtcNow,
role: ToOpenAIChatRole(chatCompletionUpdate.Role),
finishReason: ToOpenAIFinishReason(chatCompletionUpdate.FinishReason),
finishReason: chatCompletionUpdate.FinishReason is null ? null : ToOpenAIFinishReason(chatCompletionUpdate.FinishReason),
contentUpdate: [.. ToOpenAIChatContent(chatCompletionUpdate.Contents)],
toolCallUpdates: toolCallUpdates,
refusalUpdate: chatCompletionUpdate.AdditionalProperties.GetValueOrDefault<string>(nameof(OpenAI.Chat.StreamingChatCompletionUpdate.RefusalUpdate)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,38 @@ public static async Task RequestDeserialization_SimpleMessage_Stream()
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}
""";

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()
{
Expand Down Expand Up @@ -614,13 +646,13 @@ static async IAsyncEnumerable<StreamingChatCompletionUpdate> CreateStreamingComp
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":[]},"finish_reason":"stop","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 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":[]},"finish_reason":"stop","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":[]},"finish_reason":"stop","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":[]},"finish_reason":"stop","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}}}
Expand Down

0 comments on commit c63bc2c

Please sign in to comment.