1010using System . Runtime . CompilerServices ;
1111using System . Text ;
1212using System . Text . Json ;
13+ using System . Text . RegularExpressions ;
1314using System . Threading ;
1415using System . Threading . Tasks ;
1516using Microsoft . Shared . Diagnostics ;
1920#pragma warning disable CA1308 // Normalize strings to uppercase
2021#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?)
2122#pragma warning disable S1067 // Expressions should not be too complex
23+ #pragma warning disable S2333 // Unnecessary partial
2224#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
2325#pragma warning disable SA1202 // Elements should be ordered by access
26+ #pragma warning disable SA1203 // Constants should appear before fields
2427#pragma warning disable SA1204 // Static elements should appear before instance elements
2528
2629namespace Microsoft . Extensions . AI ;
2730
2831/// <summary>Represents an <see cref="IChatClient"/> for an OpenAI <see cref="OpenAIClient"/> or <see cref="ChatClient"/>.</summary>
29- internal sealed class OpenAIChatClient : IChatClient
32+ internal sealed partial class OpenAIChatClient : IChatClient
3033{
3134 // These delegate instances are used to call the internal overloads of CompleteChatAsync and CompleteChatStreamingAsync that accept
3235 // 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
157160 input . Role == OpenAIClientExtensions . ChatRoleDeveloper )
158161 {
159162 var parts = ToOpenAIChatContent ( input . Contents ) ;
163+ string ? name = SanitizeAuthorName ( input . AuthorName ) ;
160164 yield return
161- input . Role == ChatRole . System ? new SystemChatMessage ( parts ) { ParticipantName = input . AuthorName } :
162- input . Role == OpenAIClientExtensions . ChatRoleDeveloper ? new DeveloperChatMessage ( parts ) { ParticipantName = input . AuthorName } :
163- new UserChatMessage ( parts ) { ParticipantName = input . AuthorName } ;
165+ input . Role == ChatRole . System ? new SystemChatMessage ( parts ) { ParticipantName = name } :
166+ input . Role == OpenAIClientExtensions . ChatRoleDeveloper ? new DeveloperChatMessage ( parts ) { ParticipantName = name } :
167+ new UserChatMessage ( parts ) { ParticipantName = name } ;
164168 }
165169 else if ( input . Role == ChatRole . Tool )
166170 {
@@ -233,7 +237,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat
233237 new ( ChatMessageContentPart . CreateTextPart ( string . Empty ) ) ;
234238 }
235239
236- message . ParticipantName = input . AuthorName ;
240+ message . ParticipantName = SanitizeAuthorName ( input . AuthorName ) ;
237241 message . Refusal = refusal ;
238242
239243 yield return message ;
@@ -568,7 +572,6 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
568572 result . TopP ??= options . TopP ;
569573 result . PresencePenalty ??= options . PresencePenalty ;
570574 result . Temperature ??= options . Temperature ;
571- result . AllowParallelToolCalls ??= options . AllowMultipleToolCalls ;
572575 result . Seed ??= options . Seed ;
573576
574577 if ( options . StopSequences is { Count : > 0 } stopSequences )
@@ -589,6 +592,11 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
589592 }
590593 }
591594
595+ if ( result . Tools . Count > 0 )
596+ {
597+ result . AllowParallelToolCalls ??= options . AllowMultipleToolCalls ;
598+ }
599+
592600 if ( result . ToolChoice is null && result . Tools . Count > 0 )
593601 {
594602 switch ( options . ToolMode )
@@ -749,11 +757,41 @@ internal static void ConvertContentParts(ChatMessageContent content, IList<AICon
749757 _ => new ChatFinishReason ( s ) ,
750758 } ;
751759
760+ /// <summary>Sanitizes the author name to be appropriate for including as an OpenAI participant name.</summary>
761+ private static string ? SanitizeAuthorName ( string ? name )
762+ {
763+ if ( name is not null )
764+ {
765+ const int MaxLength = 64 ;
766+
767+ name = InvalidAuthorNameRegex ( ) . Replace ( name , string . Empty ) ;
768+ if ( name . Length == 0 )
769+ {
770+ name = null ;
771+ }
772+ else if ( name . Length > MaxLength )
773+ {
774+ name = name . Substring ( 0 , MaxLength ) ;
775+ }
776+ }
777+
778+ return name ;
779+ }
780+
752781 /// <summary>POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates.</summary>
753782 private sealed class FunctionCallInfo
754783 {
755784 public string ? CallId ;
756785 public string ? Name ;
757786 public StringBuilder ? Arguments ;
758787 }
788+
789+ private const string InvalidAuthorNamePattern = @"[^a-zA-Z0-9_]+" ;
790+ #if NET
791+ [ GeneratedRegex ( InvalidAuthorNamePattern ) ]
792+ private static partial Regex InvalidAuthorNameRegex ( ) ;
793+ #else
794+ private static Regex InvalidAuthorNameRegex ( ) => _invalidAuthorNameRegex ;
795+ private static readonly Regex _invalidAuthorNameRegex = new ( InvalidAuthorNamePattern , RegexOptions . Compiled ) ;
796+ #endif
759797}
0 commit comments