Skip to content

Commit 9a68639

Browse files
authored
Expose response format conversions for OpenAI types (#6806)
1 parent c4e8b3b commit 9a68639

File tree

6 files changed

+131
-38
lines changed

6 files changed

+131
-38
lines changed

src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## NOT YET RELEASED
44

5+
- Added M.E.AI to OpenAI conversions for response format types
6+
57
## 9.9.0-preview.1.25458.4
68

79
- Updated to depend on OpenAI 2.4.0

src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIChatExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ public static class MicrosoftExtensionsAIChatExtensions
2626
public static ChatTool AsOpenAIChatTool(this AIFunctionDeclaration function) =>
2727
OpenAIChatClient.ToOpenAIChatTool(Throw.IfNull(function));
2828

29+
/// <summary>
30+
/// Creates an OpenAI <see cref="ChatResponseFormat"/> from a <see cref="Microsoft.Extensions.AI.ChatResponseFormat"/>.
31+
/// </summary>
32+
/// <param name="format">The format.</param>
33+
/// <param name="options">The options to use when interpreting the format.</param>
34+
/// <returns>The converted OpenAI <see cref="ChatResponseFormat"/>.</returns>
35+
public static ChatResponseFormat? AsOpenAIChatResponseFormat(this Microsoft.Extensions.AI.ChatResponseFormat? format, ChatOptions? options = null) =>
36+
OpenAIChatClient.ToOpenAIChatResponseFormat(format, options);
37+
2938
/// <summary>Creates a sequence of OpenAI <see cref="ChatMessage"/> instances from the specified input messages.</summary>
3039
/// <param name="messages">The input messages to convert.</param>
3140
/// <param name="options">The options employed while processing <paramref name="messages"/>.</param>

src/Libraries/Microsoft.Extensions.AI.OpenAI/MicrosoftExtensionsAIResponsesExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ public static class MicrosoftExtensionsAIResponsesExtensions
2121
public static FunctionTool AsOpenAIResponseTool(this AIFunctionDeclaration function) =>
2222
OpenAIResponsesChatClient.ToResponseTool(Throw.IfNull(function));
2323

24+
/// <summary>
25+
/// Creates an OpenAI <see cref="ResponseTextFormat"/> from a <see cref="ChatResponseFormat"/>.
26+
/// </summary>
27+
/// <param name="format">The format.</param>
28+
/// <param name="options">The options to use when interpreting the format.</param>
29+
/// <returns>The converted OpenAI <see cref="ResponseTextFormat"/>.</returns>
30+
public static ResponseTextFormat? AsOpenAIResponseTextFormat(this ChatResponseFormat? format, ChatOptions? options = null) =>
31+
OpenAIResponsesChatClient.ToOpenAIResponseTextFormat(format, options);
32+
2433
/// <summary>Creates a sequence of OpenAI <see cref="ResponseItem"/> instances from the specified input messages.</summary>
2534
/// <param name="messages">The input messages to convert.</param>
2635
/// <param name="options">The options employed while processing <paramref name="messages"/>.</param>

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -582,27 +582,28 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
582582
}
583583
}
584584

585-
if (result.ResponseFormat is null)
586-
{
587-
if (options.ResponseFormat is ChatResponseFormatText)
588-
{
589-
result.ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat();
590-
}
591-
else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
592-
{
593-
result.ResponseFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ?
594-
OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
595-
jsonFormat.SchemaName ?? "json_schema",
596-
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
597-
jsonFormat.SchemaDescription,
598-
OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) :
599-
OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat();
600-
}
601-
}
585+
result.ResponseFormat ??= ToOpenAIChatResponseFormat(options.ResponseFormat, options);
602586

603587
return result;
604588
}
605589

590+
internal static OpenAI.Chat.ChatResponseFormat? ToOpenAIChatResponseFormat(ChatResponseFormat? format, ChatOptions? options) =>
591+
format switch
592+
{
593+
ChatResponseFormatText => OpenAI.Chat.ChatResponseFormat.CreateTextFormat(),
594+
595+
ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema =>
596+
OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
597+
jsonFormat.SchemaName ?? "json_schema",
598+
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
599+
jsonFormat.SchemaDescription,
600+
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties)),
601+
602+
ChatResponseFormatJson => OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat(),
603+
604+
_ => null
605+
};
606+
606607
private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage)
607608
{
608609
var destination = new UsageDetails

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -520,33 +520,32 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
520520
}
521521
}
522522

523-
if (result.TextOptions is null)
523+
if (result.TextOptions?.TextFormat is null &&
524+
ToOpenAIResponseTextFormat(options.ResponseFormat, options) is { } newFormat)
524525
{
525-
if (options.ResponseFormat is ChatResponseFormatText)
526-
{
527-
result.TextOptions = new()
528-
{
529-
TextFormat = ResponseTextFormat.CreateTextFormat()
530-
};
531-
}
532-
else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
533-
{
534-
result.TextOptions = new()
535-
{
536-
TextFormat = OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema ?
537-
ResponseTextFormat.CreateJsonSchemaFormat(
538-
jsonFormat.SchemaName ?? "json_schema",
539-
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
540-
jsonFormat.SchemaDescription,
541-
OpenAIClientExtensions.HasStrict(options.AdditionalProperties)) :
542-
ResponseTextFormat.CreateJsonObjectFormat(),
543-
};
544-
}
526+
(result.TextOptions ??= new()).TextFormat = newFormat;
545527
}
546528

547529
return result;
548530
}
549531

532+
internal static ResponseTextFormat? ToOpenAIResponseTextFormat(ChatResponseFormat? format, ChatOptions? options = null) =>
533+
format switch
534+
{
535+
ChatResponseFormatText => ResponseTextFormat.CreateTextFormat(),
536+
537+
ChatResponseFormatJson jsonFormat when OpenAIClientExtensions.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema =>
538+
ResponseTextFormat.CreateJsonSchemaFormat(
539+
jsonFormat.SchemaName ?? "json_schema",
540+
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
541+
jsonFormat.SchemaDescription,
542+
OpenAIClientExtensions.HasStrict(options?.AdditionalProperties)),
543+
544+
ChatResponseFormatJson => ResponseTextFormat.CreateJsonObjectFormat(),
545+
546+
_ => null,
547+
};
548+
550549
/// <summary>Convert a sequence of <see cref="ChatMessage"/>s to <see cref="ResponseItem"/>s.</summary>
551550
internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<ChatMessage> inputs, ChatOptions? options)
552551
{

test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.ClientModel.Primitives;
56
using System.Collections.Generic;
67
using System.ComponentModel;
78
using System.Linq;
89
using System.Text.Json;
10+
using System.Text.RegularExpressions;
911
using System.Threading.Tasks;
1012
using OpenAI.Assistants;
1113
using OpenAI.Chat;
@@ -22,6 +24,75 @@ public class OpenAIConversionTests
2224
"test_function",
2325
"A test function for conversion");
2426

27+
[Fact]
28+
public void AsOpenAIChatResponseFormat_HandlesVariousFormats()
29+
{
30+
Assert.Null(MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(null));
31+
32+
var text = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Text);
33+
Assert.NotNull(text);
34+
Assert.Equal("""{"type":"text"}""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)text).Write(ModelReaderWriterOptions.Json).ToString());
35+
36+
var json = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Json);
37+
Assert.NotNull(json);
38+
Assert.Equal("""{"type":"json_object"}""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)json).Write(ModelReaderWriterOptions.Json).ToString());
39+
40+
var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat();
41+
Assert.NotNull(jsonSchema);
42+
Assert.Equal(RemoveWhitespace("""
43+
{"type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{
44+
"$schema": "https://json-schema.org/draft/2020-12/schema",
45+
"type": "integer"
46+
}}}
47+
"""), RemoveWhitespace(((IJsonModel<OpenAI.Chat.ChatResponseFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
48+
49+
jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat(
50+
new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
51+
Assert.NotNull(jsonSchema);
52+
Assert.Equal(RemoveWhitespace("""
53+
{
54+
"type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{
55+
"$schema": "https://json-schema.org/draft/2020-12/schema",
56+
"type": "integer"
57+
},"strict":true}}
58+
"""), RemoveWhitespace(((IJsonModel<OpenAI.Chat.ChatResponseFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
59+
}
60+
61+
[Fact]
62+
public void AsOpenAIResponseTextFormat_HandlesVariousFormats()
63+
{
64+
Assert.Null(MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(null));
65+
66+
var text = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Text);
67+
Assert.NotNull(text);
68+
Assert.Equal(ResponseTextFormatKind.Text, text.Kind);
69+
70+
var json = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Json);
71+
Assert.NotNull(json);
72+
Assert.Equal(ResponseTextFormatKind.JsonObject, json.Kind);
73+
74+
var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat();
75+
Assert.NotNull(jsonSchema);
76+
Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind);
77+
Assert.Equal(RemoveWhitespace("""
78+
{"type":"json_schema","description":"A test schema","name":"my_schema","schema":{
79+
"$schema": "https://json-schema.org/draft/2020-12/schema",
80+
"type": "integer"
81+
}}
82+
"""), RemoveWhitespace(((IJsonModel<ResponseTextFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
83+
84+
jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat(
85+
new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
86+
Assert.NotNull(jsonSchema);
87+
Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind);
88+
Assert.Equal(RemoveWhitespace("""
89+
{"type":"json_schema","description":"A test schema","name":"my_schema","schema":{
90+
"$schema": "https://json-schema.org/draft/2020-12/schema",
91+
"type": "integer"
92+
},"strict":true}
93+
"""), RemoveWhitespace(((IJsonModel<ResponseTextFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString()));
94+
}
95+
2596
[Fact]
2697
public void AsOpenAIChatTool_ProducesValidInstance()
2798
{
@@ -1113,4 +1184,6 @@ private static async IAsyncEnumerable<T> CreateAsyncEnumerable<T>(IEnumerable<T>
11131184
yield return item;
11141185
}
11151186
}
1187+
1188+
private static string RemoveWhitespace(string input) => Regex.Replace(input, @"\s+", "");
11161189
}

0 commit comments

Comments
 (0)