2
2
3
3
using System ;
4
4
using System . Collections . Generic ;
5
- using System . Diagnostics . CodeAnalysis ;
6
5
using System . Linq ;
7
6
using System . Net . Http ;
8
7
using System . Text ;
21
20
22
21
namespace Microsoft . SemanticKernel . Connectors . AI . OpenAI . CustomClient ;
23
22
24
- /// <summary>
25
- /// An abstract OpenAI Client.
26
- /// </summary>
27
- [ SuppressMessage ( "Design" , "CA1054:URI-like parameters should not be strings" , Justification = " OpenAI users use strings" ) ]
23
+ #pragma warning disable CA1063 // Class isn't publicly extensible and thus doesn't implement the full IDisposable pattern
24
+ #pragma warning disable CA1816 // No derived types implement a finalizer
25
+
26
+ /// <summary>Base type for OpenAI clients.</summary>
28
27
public abstract class OpenAIClientBase : IDisposable
29
28
{
30
- protected static readonly HttpClientHandler DefaultHttpClientHandler = new ( ) { CheckCertificateRevocationList = true } ;
31
-
32
- /// <summary>
33
- /// Logger
34
- /// </summary>
35
- protected ILogger Log { get ; } = NullLogger . Instance ;
29
+ /// <summary>Initialize the client.</summary>
30
+ private protected OpenAIClientBase ( HttpClient ? httpClient = null , ILogger ? logger = null )
31
+ {
32
+ this . _httpClient = httpClient ?? new HttpClient ( s_defaultHttpClientHandler , disposeHandler : false ) ;
33
+ this . _disposeHttpClient = this . _httpClient != httpClient ; // dispose a non-shared client when this is disposed
36
34
37
- /// <summary>
38
- /// HTTP client
39
- /// </summary>
40
- protected HttpClient HTTPClient { get ; }
35
+ this . _log = logger ?? NullLogger . Instance ;
36
+ }
41
37
42
- internal OpenAIClientBase ( HttpClient ? httpClient = null , ILogger ? logger = null )
38
+ /// <summary>Clean up resources used by this instance.</summary>
39
+ public void Dispose ( )
43
40
{
44
- this . Log = logger ?? this . Log ;
45
-
46
- if ( httpClient == null )
47
- {
48
- this . HTTPClient = new HttpClient ( DefaultHttpClientHandler , disposeHandler : false ) ;
49
- this . _disposeHttpClient = true ; // If client is created internally, dispose it when done
50
- }
51
- else
41
+ if ( this . _disposeHttpClient )
52
42
{
53
- this . HTTPClient = httpClient ;
43
+ this . _httpClient . Dispose ( ) ;
54
44
}
45
+ }
55
46
56
- this . HTTPClient . DefaultRequestHeaders . Add ( "User-Agent" , HTTPUserAgent ) ;
47
+ /// <summary>Adds headers to use for OpenAI HTTP requests.</summary>
48
+ private protected virtual void AddRequestHeaders ( HttpRequestMessage request )
49
+ {
50
+ request . Headers . Add ( "User-Agent" , HttpUserAgent ) ;
57
51
}
58
52
59
53
/// <summary>
@@ -64,112 +58,43 @@ internal OpenAIClientBase(HttpClient? httpClient = null, ILogger? logger = null)
64
58
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
65
59
/// <returns>List of text embeddings</returns>
66
60
/// <exception cref="AIException">AIException thrown during the request.</exception>
67
- protected async Task < IList < Embedding < float > > > ExecuteTextEmbeddingRequestAsync (
61
+ private protected async Task < IList < Embedding < float > > > ExecuteTextEmbeddingRequestAsync (
68
62
string url ,
69
63
string requestBody ,
70
64
CancellationToken cancellationToken = default )
71
65
{
72
- try
73
- {
74
- var result = await this . ExecutePostRequestAsync < TextEmbeddingResponse > ( url , requestBody , cancellationToken ) . ConfigureAwait ( false ) ;
75
- if ( result . Embeddings . Count < 1 )
76
- {
77
- throw new AIException (
78
- AIException . ErrorCodes . InvalidResponseContent ,
79
- "Embeddings not found" ) ;
80
- }
81
-
82
- return result . Embeddings . Select ( e => new Embedding < float > ( e . Values ) ) . ToList ( ) ;
83
- }
84
- catch ( Exception e ) when ( e is not AIException )
66
+ var result = await this . ExecutePostRequestAsync < TextEmbeddingResponse > ( url , requestBody , cancellationToken ) . ConfigureAwait ( false ) ;
67
+ if ( result . Embeddings is not { Count : >= 1 } )
85
68
{
86
69
throw new AIException (
87
- AIException . ErrorCodes . UnknownError ,
88
- $ "Something went wrong: { e . Message } " , e ) ;
70
+ AIException . ErrorCodes . InvalidResponseContent ,
71
+ "Embeddings not found" ) ;
89
72
}
90
- }
91
73
92
- /// <summary>
93
- /// Run the HTTP request to generate a list of images
94
- /// </summary>
95
- /// <param name="url">URL for the image generation request API</param>
96
- /// <param name="requestBody">Request payload</param>
97
- /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
98
- /// <returns>List of image URLs</returns>
99
- /// <exception cref="AIException">AIException thrown during the request.</exception>
100
- protected async Task < IList < string > > ExecuteImageUrlGenerationRequestAsync (
101
- string url ,
102
- string requestBody ,
103
- CancellationToken cancellationToken = default )
104
- {
105
- try
106
- {
107
- var result = await this . ExecutePostRequestAsync < ImageGenerationResponse > ( url , requestBody , cancellationToken ) . ConfigureAwait ( false ) ;
108
- return result . Images . Select ( x => x . Url ) . ToList ( ) ;
109
- }
110
- catch ( Exception e ) when ( e is not AIException )
111
- {
112
- throw new AIException (
113
- AIException . ErrorCodes . UnknownError ,
114
- $ "Something went wrong: { e . Message } ", e ) ;
115
- }
74
+ return result . Embeddings . Select ( e => new Embedding < float > ( e . Values ) ) . ToList ( ) ;
116
75
}
117
76
118
77
/// <summary>
119
78
/// Run the HTTP request to generate a list of images
120
79
/// </summary>
121
80
/// <param name="url">URL for the image generation request API</param>
122
81
/// <param name="requestBody">Request payload</param>
82
+ /// <param name="extractResponseFunc">Function to invoke to extract the desired portion of the image generation response.</param>
123
83
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
124
- /// <returns>List of images serialized in base64 </returns>
84
+ /// <returns>List of image URLs </returns>
125
85
/// <exception cref="AIException">AIException thrown during the request.</exception>
126
- protected async Task < IList < string > > ExecuteImageBase64GenerationRequestAsync (
86
+ private protected async Task < IList < string > > ExecuteImageGenerationRequestAsync (
127
87
string url ,
128
88
string requestBody ,
89
+ Func < ImageGenerationResponse . Image , string > extractResponseFunc ,
129
90
CancellationToken cancellationToken = default )
130
91
{
131
- try
132
- {
133
- var result = await this . ExecutePostRequestAsync < ImageGenerationResponse > ( url , requestBody , cancellationToken ) . ConfigureAwait ( false ) ;
134
- return result . Images . Select ( x => x . AsBase64 ) . ToList ( ) ;
135
- }
136
- catch ( Exception e ) when ( e is not AIException )
137
- {
138
- throw new AIException (
139
- AIException . ErrorCodes . UnknownError ,
140
- $ "Something went wrong: { e . Message } ", e ) ;
141
- }
142
- }
143
-
144
- /// <summary>
145
- /// Explicit finalizer called by IDisposable
146
- /// </summary>
147
- public void Dispose ( )
148
- {
149
- this . Dispose ( true ) ;
150
- // Request CL runtime not to call the finalizer - reduce cost of GC
151
- GC . SuppressFinalize ( this ) ;
152
- }
153
-
154
- /// <summary>
155
- /// Overridable finalizer for concrete classes
156
- /// </summary>
157
- /// <param name="disposing"></param>
158
- protected virtual void Dispose ( bool disposing )
159
- {
160
- if ( disposing & this . _disposeHttpClient )
161
- {
162
- this . HTTPClient . Dispose ( ) ;
163
- }
92
+ var result = await this . ExecutePostRequestAsync < ImageGenerationResponse > ( url , requestBody , cancellationToken ) . ConfigureAwait ( false ) ;
93
+ return result . Images . Select ( extractResponseFunc ) . ToList ( ) ;
164
94
}
165
95
166
- protected virtual string ? GetErrorMessageFromResponse ( string ? jsonResponsePayload )
96
+ private protected virtual string ? GetErrorMessageFromResponse ( string jsonResponsePayload )
167
97
{
168
- if ( jsonResponsePayload is null )
169
- {
170
- return null ;
171
- }
172
-
173
98
try
174
99
{
175
100
JsonNode ? root = JsonSerializer . Deserialize < JsonNode > ( jsonResponsePayload ) ;
@@ -178,33 +103,48 @@ protected virtual void Dispose(bool disposing)
178
103
}
179
104
catch ( Exception ex ) when ( ex is NotSupportedException or JsonException )
180
105
{
181
- this . Log . LogTrace ( "Unable to extract error from response body content. Exception: {0}:{1}" , ex . GetType ( ) , ex . Message ) ;
182
- return null ;
106
+ this . _log . LogTrace ( "Unable to extract error from response body content. Exception: {0}:{1}" , ex . GetType ( ) , ex . Message ) ;
183
107
}
108
+
109
+ return null ;
184
110
}
185
111
186
112
#region private ================================================================================
187
113
114
+ // Shared singleton HttpClientHandler used when an existing HttpClient isn't provided
115
+ private static readonly HttpClientHandler s_defaultHttpClientHandler = new ( ) { CheckCertificateRevocationList = true } ;
116
+
188
117
// HTTP user agent sent to remote endpoints
189
- private const string HTTPUserAgent = "Microsoft-Semantic-Kernel" ;
118
+ private const string HttpUserAgent = "Microsoft-Semantic-Kernel" ;
190
119
191
120
// Set to true to dispose of HttpClient when disposing. If HttpClient was passed in, then the caller can manage.
192
- private readonly bool _disposeHttpClient = false ;
121
+ private readonly bool _disposeHttpClient ;
122
+
123
+ /// <summary>
124
+ /// Logger
125
+ /// </summary>
126
+ private readonly ILogger _log ;
127
+
128
+ /// <summary>
129
+ /// The <see cref="_httpClient"/> to use for issuing requests.
130
+ /// </summary>
131
+ private readonly HttpClient _httpClient ;
193
132
194
133
private async Task < T > ExecutePostRequestAsync < T > ( string url , string requestBody , CancellationToken cancellationToken = default )
195
134
{
196
- string responseJson ;
197
-
135
+ HttpResponseMessage ? response = null ;
198
136
try
199
137
{
200
- using HttpContent content = new StringContent ( requestBody , Encoding . UTF8 , "application/json" ) ;
201
-
202
- HttpResponseMessage response = await this . HTTPClient . PostAsync ( url , content , cancellationToken ) . ConfigureAwait ( false )
203
- ?? throw new AIException ( AIException . ErrorCodes . NoResponse ) ;
138
+ using ( var request = new HttpRequestMessage ( HttpMethod . Post , url ) )
139
+ {
140
+ this . AddRequestHeaders ( request ) ;
141
+ request . Content = new StringContent ( requestBody , Encoding . UTF8 , "application/json" ) ;
142
+ response = await this . _httpClient . SendAsync ( request , cancellationToken ) . ConfigureAwait ( false ) ;
143
+ }
204
144
205
- this . Log . LogTrace ( "HTTP response: {0} {1}" , ( int ) response . StatusCode , response . StatusCode . ToString ( "G" ) ) ;
145
+ this . _log . LogTrace ( "HTTP response: {0} {1}" , ( int ) response . StatusCode , response . StatusCode . ToString ( "G" ) ) ;
206
146
207
- responseJson = await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ;
147
+ string responseJson = await response . Content . ReadAsStringAsync ( ) . ConfigureAwait ( false ) ;
208
148
string ? errorDetail = this . GetErrorMessageFromResponse ( responseJson ) ;
209
149
210
150
if ( ! response . IsSuccessStatusCode )
@@ -276,29 +216,27 @@ private async Task<T> ExecutePostRequestAsync<T>(string url, string requestBody,
276
216
errorDetail ) ;
277
217
}
278
218
}
279
- }
280
- catch ( Exception e ) when ( e is not AIException )
281
- {
282
- throw new AIException (
283
- AIException . ErrorCodes . UnknownError ,
284
- $ "Something went wrong: { e . Message } ", e ) ;
285
- }
286
219
287
- try
288
- {
289
220
var result = Json . Deserialize < T > ( responseJson ) ;
290
- if ( result != null ) { return result ; }
291
-
292
- throw new AIException (
221
+ if ( result is null )
222
+ {
223
+ throw new AIException (
293
224
AIException . ErrorCodes . InvalidResponseContent ,
294
225
"Response JSON parse error" ) ;
226
+ }
227
+
228
+ return result ;
295
229
}
296
230
catch ( Exception e ) when ( e is not AIException )
297
231
{
298
232
throw new AIException (
299
233
AIException . ErrorCodes . UnknownError ,
300
234
$ "Something went wrong: { e . Message } ", e ) ;
301
235
}
236
+ finally
237
+ {
238
+ response ? . Dispose ( ) ;
239
+ }
302
240
}
303
241
304
242
#endregion
0 commit comments