Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## NOT YET RELEASED

- Added M.E.AI to OpenAI conversions for response format types

## 9.9.0-preview.1.25458.4

- Updated to depend on OpenAI 2.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ public static class MicrosoftExtensionsAIChatExtensions
public static ChatTool AsOpenAIChatTool(this AIFunctionDeclaration function) =>
OpenAIChatClient.ToOpenAIChatTool(Throw.IfNull(function));

/// <summary>
/// Creates an OpenAI <see cref="ChatResponseFormat"/> from a <see cref="Microsoft.Extensions.AI.ChatResponseFormat"/>.
/// </summary>
/// <param name="format">The format.</param>
/// <param name="options">The options to use when interpreting the format.</param>
/// <returns>The converted OpenAI <see cref="ChatResponseFormat"/>.</returns>
public static ChatResponseFormat? AsOpenAIChatResponseFormat(this Microsoft.Extensions.AI.ChatResponseFormat? format, ChatOptions? options = null) =>
OpenAIChatClient.ToOpenAIChatResponseFormat(format, options);

/// <summary>Creates a sequence of OpenAI <see cref="ChatMessage"/> instances from the specified input messages.</summary>
/// <param name="messages">The input messages to convert.</param>
/// <param name="options">The options employed while processing <paramref name="messages"/>.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ public static class MicrosoftExtensionsAIResponsesExtensions
public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration function) =>
OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function));

/// <summary>
/// Creates an OpenAI <see cref="ResponseTextFormat"/> from a <see cref="ChatResponseFormat"/>.
/// </summary>
/// <param name="format">The format.</param>
/// <param name="options">The options to use when interpreting the format.</param>
/// <returns>The converted OpenAI <see cref="ResponseTextFormat"/>.</returns>
public static ResponseTextFormat? AsOpenAIResponseTextFormat(this ChatResponseFormat? format, ChatOptions? options = null) =>
OpenAIResponsesChatClient.ToOpenAIResponseTextFormat(format, options);

/// <summary>Creates a sequence of OpenAI <see cref="ResponseItem"/> instances from the specified input messages.</summary>
/// <param name="messages">The input messages to convert.</param>
/// <param name="options">The options employed while processing <paramref name="messages"/>.</param>
Expand Down
35 changes: 18 additions & 17 deletions src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,27 +582,28 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
}
}

if (result.ResponseFormat is null)
{
if (options.ResponseFormat is ChatResponseFormatText)
{
result.ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat();
}
else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
{
result.ResponseFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ?
OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
jsonFormat.SchemaName ?? "json_schema",
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
jsonFormat.SchemaDescription,
OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) :
OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat();
}
}
result.ResponseFormat ??= ToOpenAIChatResponseFormat(options.ResponseFormat, options);

return result;
}

internal static OpenAI.Chat.ChatResponseFormat? ToOpenAIChatResponseFormat(ChatResponseFormat? format, ChatOptions? options) =>
format switch
{
ChatResponseFormatText => OpenAI.Chat.ChatResponseFormat.CreateTextFormat(),

ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema =>
OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
jsonFormat.SchemaName ?? "json_schema",
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
jsonFormat.SchemaDescription,
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties)),

ChatResponseFormatJson => OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat(),

_ => null
};

private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage)
{
var destination = new UsageDetails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,33 +520,32 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
}
}

if (result.TextOptions is null)
if (result.TextOptions?.TextFormat is null &&
ToOpenAIResponseTextFormat(options.ResponseFormat, options) is { } newFormat)
{
if (options.ResponseFormat is ChatResponseFormatText)
{
result.TextOptions = new()
{
TextFormat = ResponseTextFormat.CreateTextFormat()
};
}
else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
{
result.TextOptions = new()
{
TextFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ?
ResponseTextFormat.CreateJsonSchemaFormat(
jsonFormat.SchemaName ?? "json_schema",
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
jsonFormat.SchemaDescription,
OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) :
ResponseTextFormat.CreateJsonObjectFormat(),
};
}
(result.TextOptions ??= new()).TextFormat = newFormat;
}

return result;
}

internal static ResponseTextFormat? ToOpenAIResponseTextFormat(ChatResponseFormat? format, ChatOptions? options = null) =>
format switch
{
ChatResponseFormatText => ResponseTextFormat.CreateTextFormat(),

ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema =>
ResponseTextFormat.CreateJsonSchemaFormat(
jsonFormat.SchemaName ?? "json_schema",
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
jsonFormat.SchemaDescription,
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties)),

ChatResponseFormatJson => ResponseTextFormat.CreateJsonObjectFormat(),

_ => null,
};

/// <summary>Convert a sequence of <see cref="ChatMessage"/>s to <see cref="ResponseItem"/>s.</summary>
internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<ChatMessage> inputs, ChatOptions? options)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using OpenAI.Assistants;
using OpenAI.Chat;
Expand All @@ -22,6 +24,75 @@ public class OpenAIConversionTests
"test_function",
"A test function for conversion");

[Fact]
public void AsOpenAIChatResponseFormat_HandlesVariousFormats()
{
Assert.Null(MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(null));

var text = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Text);
Assert.NotNull(text);
Assert.Equal("""{"type":"text"}""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)text).Write(ModelReaderWriterOptions.Json).ToString());

var json = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Json);
Assert.NotNull(json);
Assert.Equal("""{"type":"json_object"}""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)json).Write(ModelReaderWriterOptions.Json).ToString());

var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat();
Assert.NotNull(jsonSchema);
Assert.Equal(RemoveWhitespace("""
{"type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer"
}}}
"""), RemoveWhitespace(((IJsonModel<OpenAI.Chat.ChatResponseFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));

jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat(
new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
Assert.NotNull(jsonSchema);
Assert.Equal(RemoveWhitespace("""
{
"type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer"
},"strict":true}}
"""), RemoveWhitespace(((IJsonModel<OpenAI.Chat.ChatResponseFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
}

[Fact]
public void AsOpenAIResponseTextFormat_HandlesVariousFormats()
{
Assert.Null(MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(null));

var text = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Text);
Assert.NotNull(text);
Assert.Equal(ResponseTextFormatKind.Text, text.Kind);

var json = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Json);
Assert.NotNull(json);
Assert.Equal(ResponseTextFormatKind.JsonObject, json.Kind);

var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat();
Assert.NotNull(jsonSchema);
Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind);
Assert.Equal(RemoveWhitespace("""
{"type":"json_schema","description":"A test schema","name":"my_schema","schema":{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer"
}}
"""), RemoveWhitespace(((IJsonModel<ResponseTextFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));

jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat(
new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
Assert.NotNull(jsonSchema);
Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind);
Assert.Equal(RemoveWhitespace("""
{"type":"json_schema","description":"A test schema","name":"my_schema","schema":{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer"
},"strict":true}
"""), RemoveWhitespace(((IJsonModel<ResponseTextFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
}

[Fact]
public void AsOpenAIChatTool_ProducesValidInstance()
{
Expand Down Expand Up @@ -1113,4 +1184,6 @@ private static async IAsyncEnumerable<T> CreateAsyncEnumerable<T>(IEnumerable<T>
yield return item;
}
}

private static string RemoveWhitespace(string input) => Regex.Replace(input, @"\s+", "");
}
Loading