Skip to content

Commit 5def8e3

Browse files
committed
Expose response format conversions for OpenAI types
1 parent d299e16 commit 5def8e3

File tree

6 files changed

+127
-38
lines changed

6 files changed

+127
-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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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;
@@ -22,6 +23,74 @@ public class OpenAIConversionTests
2223
"test_function",
2324
"A test function for conversion");
2425

26+
[Fact]
27+
public void AsOpenAIChatResponseFormat_HandlesVariousFormats()
28+
{
29+
Assert.Null(MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(null));
30+
31+
var text = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Text);
32+
Assert.NotNull(text);
33+
Assert.Equal("""{"type":"text"}""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)text).Write(ModelReaderWriterOptions.Json).ToString());
34+
35+
var json = MicrosoftExtensionsAIChatExtensions.AsOpenAIChatResponseFormat(ChatResponseFormat.Json);
36+
Assert.NotNull(json);
37+
Assert.Equal("""{"type":"json_object"}""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)json).Write(ModelReaderWriterOptions.Json).ToString());
38+
39+
var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat();
40+
Assert.NotNull(jsonSchema);
41+
Assert.Equal("""
42+
{"type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{
43+
"$schema": "https://json-schema.org/draft/2020-12/schema",
44+
"type": "integer"
45+
}}}
46+
""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString());
47+
48+
jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIChatResponseFormat(
49+
new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
50+
Assert.NotNull(jsonSchema);
51+
Assert.Equal("""
52+
{"type":"json_schema","json_schema":{"description":"A test schema","name":"my_schema","schema":{
53+
"$schema": "https://json-schema.org/draft/2020-12/schema",
54+
"type": "integer"
55+
},"strict":true}}
56+
""", ((IJsonModel<OpenAI.Chat.ChatResponseFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString());
57+
}
58+
59+
[Fact]
60+
public void AsOpenAIResponseTextFormat_HandlesVariousFormats()
61+
{
62+
Assert.Null(MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(null));
63+
64+
var text = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Text);
65+
Assert.NotNull(text);
66+
Assert.Equal(ResponseTextFormatKind.Text, text.Kind);
67+
68+
var json = MicrosoftExtensionsAIResponsesExtensions.AsOpenAIResponseTextFormat(ChatResponseFormat.Json);
69+
Assert.NotNull(json);
70+
Assert.Equal(ResponseTextFormatKind.JsonObject, json.Kind);
71+
72+
var jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat();
73+
Assert.NotNull(jsonSchema);
74+
Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind);
75+
Assert.Equal("""
76+
{"type":"json_schema","description":"A test schema","name":"my_schema","schema":{
77+
"$schema": "https://json-schema.org/draft/2020-12/schema",
78+
"type": "integer"
79+
}}
80+
""", ((IJsonModel<ResponseTextFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString());
81+
82+
jsonSchema = ChatResponseFormat.ForJsonSchema(typeof(int), schemaName: "my_schema", schemaDescription: "A test schema").AsOpenAIResponseTextFormat(
83+
new() { AdditionalProperties = new AdditionalPropertiesDictionary { ["strictJsonSchema"] = true } });
84+
Assert.NotNull(jsonSchema);
85+
Assert.Equal(ResponseTextFormatKind.JsonSchema, jsonSchema.Kind);
86+
Assert.Equal("""
87+
{"type":"json_schema","description":"A test schema","name":"my_schema","schema":{
88+
"$schema": "https://json-schema.org/draft/2020-12/schema",
89+
"type": "integer"
90+
},"strict":true}
91+
""", ((IJsonModel<ResponseTextFormat>)jsonSchema).Write(ModelReaderWriterOptions.Json).ToString());
92+
}
93+
2594
[Fact]
2695
public void AsOpenAIChatTool_ProducesValidInstance()
2796
{

0 commit comments

Comments
 (0)