diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
index a50aea8dea3..d531f3dd445 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
@@ -4,6 +4,9 @@
- Added M.E.AI to OpenAI conversions for response format types.
- Added `ResponseTool` to `AITool` conversions.
+- Fixed the handling of `HostedCodeInterpreterTool` with Responses when no file IDs were provided.
+- Fixed an issue where requests would fail when AllowMultipleToolCalls was set with no tools provided.
+- Fixed an issue where requests would fail when an AuthorName was provided containing invalid characters.
## 9.9.0-preview.1.25458.4
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
index fb0ee48a236..33f22dda420 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
@@ -10,6 +10,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
@@ -19,14 +20,16 @@
#pragma warning disable CA1308 // Normalize strings to uppercase
#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?)
#pragma warning disable S1067 // Expressions should not be too complex
+#pragma warning disable S2333 // Unnecessary partial
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
#pragma warning disable SA1202 // Elements should be ordered by access
+#pragma warning disable SA1203 // Constants should appear before fields
#pragma warning disable SA1204 // Static elements should appear before instance elements
namespace Microsoft.Extensions.AI;
/// Represents an for an OpenAI or .
-internal sealed class OpenAIChatClient : IChatClient
+internal sealed partial class OpenAIChatClient : IChatClient
{
// These delegate instances are used to call the internal overloads of CompleteChatAsync and CompleteChatStreamingAsync that accept
// a RequestOptions. These should be replaced once a better way to pass RequestOptions is available.
@@ -157,10 +160,11 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat
input.Role == OpenAIClientExtensions.ChatRoleDeveloper)
{
var parts = ToOpenAIChatContent(input.Contents);
+ string? name = SanitizeAuthorName(input.AuthorName);
yield return
- input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = input.AuthorName } :
- input.Role == OpenAIClientExtensions.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } :
- new UserChatMessage(parts) { ParticipantName = input.AuthorName };
+ input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = name } :
+ input.Role == OpenAIClientExtensions.ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = name } :
+ new UserChatMessage(parts) { ParticipantName = name };
}
else if (input.Role == ChatRole.Tool)
{
@@ -233,7 +237,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat
new(ChatMessageContentPart.CreateTextPart(string.Empty));
}
- message.ParticipantName = input.AuthorName;
+ message.ParticipantName = SanitizeAuthorName(input.AuthorName);
message.Refusal = refusal;
yield return message;
@@ -568,7 +572,6 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
result.TopP ??= options.TopP;
result.PresencePenalty ??= options.PresencePenalty;
result.Temperature ??= options.Temperature;
- result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls;
result.Seed ??= options.Seed;
if (options.StopSequences is { Count: > 0 } stopSequences)
@@ -589,6 +592,11 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
}
}
+ if (result.Tools.Count > 0)
+ {
+ result.AllowParallelToolCalls ??= options.AllowMultipleToolCalls;
+ }
+
if (result.ToolChoice is null && result.Tools.Count > 0)
{
switch (options.ToolMode)
@@ -749,6 +757,27 @@ internal static void ConvertContentParts(ChatMessageContent content, IList new ChatFinishReason(s),
};
+ /// Sanitizes the author name to be appropriate for including as an OpenAI participant name.
+ private static string? SanitizeAuthorName(string? name)
+ {
+ if (name is not null)
+ {
+ const int MaxLength = 64;
+
+ name = InvalidAuthorNameRegex().Replace(name, string.Empty);
+ if (name.Length == 0)
+ {
+ name = null;
+ }
+ else if (name.Length > MaxLength)
+ {
+ name = name.Substring(0, MaxLength);
+ }
+ }
+
+ return name;
+ }
+
/// POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates.
private sealed class FunctionCallInfo
{
@@ -756,4 +785,13 @@ private sealed class FunctionCallInfo
public string? Name;
public StringBuilder? Arguments;
}
+
+ private const string InvalidAuthorNamePattern = @"[^a-zA-Z0-9_]+";
+#if NET
+ [GeneratedRegex(InvalidAuthorNamePattern)]
+ private static partial Regex InvalidAuthorNameRegex();
+#else
+ private static Regex InvalidAuthorNameRegex() => _invalidAuthorNameRegex;
+ private static readonly Regex _invalidAuthorNameRegex = new(InvalidAuthorNamePattern, RegexOptions.Compiled);
+#endif
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
index d094f5f8581..aa9807d16e6 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs
@@ -418,7 +418,6 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
// Handle strongly-typed properties.
result.MaxOutputTokenCount ??= options.MaxOutputTokens;
- result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls;
result.PreviousResponseId ??= options.ConversationId;
result.Temperature ??= options.Temperature;
result.TopP ??= options.TopP;
@@ -530,6 +529,11 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
}
}
+ if (result.Tools.Count > 0)
+ {
+ result.ParallelToolCallsEnabled ??= options.AllowMultipleToolCalls;
+ }
+
if (result.ToolChoice is null && result.Tools.Count > 0)
{
switch (options.ToolMode)
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
index bdf3b2e7c0a..eca94315fa0 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIChatClientTests.cs
@@ -152,6 +152,7 @@ public async Task BasicRequestResponse_NonStreaming()
var response = await client.GetResponseAsync("hello", new()
{
+ AllowMultipleToolCalls = false,
MaxOutputTokens = 10,
Temperature = 0.5f,
});
@@ -658,15 +659,46 @@ public async Task StronglyTypedOptions_AllSent()
{
const string Input = """
{
- "messages":[{"role":"user","content":"hello"}],
- "model":"gpt-4o-mini",
- "logprobs":true,
- "top_logprobs":42,
- "logit_bias":{"12":34},
- "parallel_tool_calls":false,
- "user":"12345",
- "metadata":{"something":"else"},
- "store":true
+ "metadata": {
+ "something": "else"
+ },
+ "user": "12345",
+ "messages": [
+ {
+ "role": "user",
+ "content": "hello"
+ }
+ ],
+ "model": "gpt-4o-mini",
+ "top_logprobs": 42,
+ "store": true,
+ "logit_bias": {
+ "12": 34
+ },
+ "logprobs": true,
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "description": "",
+ "name": "GetPersonAge",
+ "parameters": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ ],
+ "tool_choice": "auto",
+ "parallel_tool_calls": false
}
""";
@@ -694,6 +726,7 @@ public async Task StronglyTypedOptions_AllSent()
Assert.NotNull(await client.GetResponseAsync("hello", new()
{
AllowMultipleToolCalls = false,
+ Tools = [AIFunctionFactory.Create((string name) => 42, "GetPersonAge")],
RawRepresentationFactory = (c) =>
{
var openAIOptions = new ChatCompletionOptions
diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
index 7724ad98360..ed00f856667 100644
--- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIConversionTests.cs
@@ -159,7 +159,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
List messages =
[
new(ChatRole.System, "You are a helpful assistant."),
- new(ChatRole.User, "Hello"),
+ new(ChatRole.User, "Hello") { AuthorName = "Jane" },
new(ChatRole.Assistant,
[
new TextContent("Hi there!"),
@@ -168,9 +168,9 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
["param1"] = "value1",
["param2"] = 42
}),
- ]),
+ ]) { AuthorName = "!@#$%John Smith^*)" },
new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]),
- new(ChatRole.Assistant, "The answer is 42."),
+ new(ChatRole.Assistant, "The answer is 42.") { AuthorName = "@#$#$@$" },
];
ChatOptions? options = withOptions ? new ChatOptions { Instructions = "You talk like a parrot." } : null;
@@ -196,6 +196,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
UserChatMessage m1 = Assert.IsType(convertedMessages[index + 1], exactMatch: false);
Assert.Equal("Hello", Assert.Single(m1.Content).Text);
+ Assert.Equal("Jane", m1.ParticipantName);
AssistantChatMessage m2 = Assert.IsType(convertedMessages[index + 2], exactMatch: false);
Assert.Single(m2.Content);
@@ -208,6 +209,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
["param1"] = "value1",
["param2"] = 42
}), JsonSerializer.Deserialize(tc.FunctionArguments.ToMemory().Span)));
+ Assert.Equal("JohnSmith", m2.ParticipantName);
ToolChatMessage m3 = Assert.IsType(convertedMessages[index + 3], exactMatch: false);
Assert.Equal("callid123", m3.ToolCallId);
@@ -215,6 +217,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
AssistantChatMessage m4 = Assert.IsType(convertedMessages[index + 4], exactMatch: false);
Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text);
+ Assert.Null(m4.ParticipantName);
}
[Fact]