10
10
using System . Runtime . CompilerServices ;
11
11
using System . Text ;
12
12
using System . Text . Json ;
13
+ using System . Text . RegularExpressions ;
13
14
using System . Threading ;
14
15
using System . Threading . Tasks ;
15
16
using Microsoft . Shared . Diagnostics ;
19
20
#pragma warning disable CA1308 // Normalize strings to uppercase
20
21
#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?)
21
22
#pragma warning disable S1067 // Expressions should not be too complex
23
+ #pragma warning disable S2333 // Unnecessary partial
22
24
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
23
25
#pragma warning disable SA1202 // Elements should be ordered by access
26
+ #pragma warning disable SA1203 // Constants should appear before fields
24
27
#pragma warning disable SA1204 // Static elements should appear before instance elements
25
28
26
29
namespace Microsoft . Extensions . AI ;
27
30
28
31
/// <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
30
33
{
31
34
// These delegate instances are used to call the internal overloads of CompleteChatAsync and CompleteChatStreamingAsync that accept
32
35
// 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
157
160
input . Role == OpenAIClientExtensions . ChatRoleDeveloper )
158
161
{
159
162
var parts = ToOpenAIChatContent ( input . Contents ) ;
163
+ string ? name = SanitizeAuthorName ( input . AuthorName ) ;
160
164
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 } ;
164
168
}
165
169
else if ( input . Role == ChatRole . Tool )
166
170
{
@@ -233,7 +237,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunctionDeclaration aiFunction, Chat
233
237
new ( ChatMessageContentPart . CreateTextPart ( string . Empty ) ) ;
234
238
}
235
239
236
- message . ParticipantName = input . AuthorName ;
240
+ message . ParticipantName = SanitizeAuthorName ( input . AuthorName ) ;
237
241
message . Refusal = refusal ;
238
242
239
243
yield return message ;
@@ -568,7 +572,6 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
568
572
result . TopP ??= options . TopP ;
569
573
result . PresencePenalty ??= options . PresencePenalty ;
570
574
result . Temperature ??= options . Temperature ;
571
- result . AllowParallelToolCalls ??= options . AllowMultipleToolCalls ;
572
575
result . Seed ??= options . Seed ;
573
576
574
577
if ( options . StopSequences is { Count : > 0 } stopSequences )
@@ -589,6 +592,11 @@ private ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
589
592
}
590
593
}
591
594
595
+ if ( result . Tools . Count > 0 )
596
+ {
597
+ result . AllowParallelToolCalls ??= options . AllowMultipleToolCalls ;
598
+ }
599
+
592
600
if ( result . ToolChoice is null && result . Tools . Count > 0 )
593
601
{
594
602
switch ( options . ToolMode )
@@ -749,11 +757,41 @@ internal static void ConvertContentParts(ChatMessageContent content, IList<AICon
749
757
_ => new ChatFinishReason ( s ) ,
750
758
} ;
751
759
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
+
752
781
/// <summary>POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates.</summary>
753
782
private sealed class FunctionCallInfo
754
783
{
755
784
public string ? CallId ;
756
785
public string ? Name ;
757
786
public StringBuilder ? Arguments ;
758
787
}
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
759
797
}
0 commit comments