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
3 changes: 3 additions & 0 deletions src/Libraries/Microsoft.Extensions.AI.OpenAI/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/// <summary>Represents an <see cref="IChatClient"/> for an OpenAI <see cref="OpenAIClient"/> or <see cref="ChatClient"/>.</summary>
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.
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -749,11 +757,41 @@ internal static void ConvertContentParts(ChatMessageContent content, IList<AICon
_ => new ChatFinishReason(s),
};

/// <summary>Sanitizes the author name to be appropriate for including as an OpenAI participant name.</summary>
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;
}

/// <summary>POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates.</summary>
private sealed class FunctionCallInfo
{
public string? CallId;
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public async Task BasicRequestResponse_NonStreaming()

var response = await client.GetResponseAsync("hello", new()
{
AllowMultipleToolCalls = false,
MaxOutputTokens = 10,
Temperature = 0.5f,
});
Expand Down Expand Up @@ -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
}
""";

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
List<ChatMessage> 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!"),
Expand All @@ -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;
Expand All @@ -196,6 +196,7 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)

UserChatMessage m1 = Assert.IsType<UserChatMessage>(convertedMessages[index + 1], exactMatch: false);
Assert.Equal("Hello", Assert.Single(m1.Content).Text);
Assert.Equal("Jane", m1.ParticipantName);

AssistantChatMessage m2 = Assert.IsType<AssistantChatMessage>(convertedMessages[index + 2], exactMatch: false);
Assert.Single(m2.Content);
Expand All @@ -208,13 +209,15 @@ public void AsOpenAIChatMessages_ProducesExpectedOutput(bool withOptions)
["param1"] = "value1",
["param2"] = 42
}), JsonSerializer.Deserialize<JsonElement>(tc.FunctionArguments.ToMemory().Span)));
Assert.Equal("JohnSmith", m2.ParticipantName);

ToolChatMessage m3 = Assert.IsType<ToolChatMessage>(convertedMessages[index + 3], exactMatch: false);
Assert.Equal("callid123", m3.ToolCallId);
Assert.Equal("theresult", Assert.Single(m3.Content).Text);

AssistantChatMessage m4 = Assert.IsType<AssistantChatMessage>(convertedMessages[index + 4], exactMatch: false);
Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text);
Assert.Null(m4.ParticipantName);
}

[Fact]
Expand Down
Loading