openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/customize-openairesponsescontext-attribute

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

OpenAI/src/Custom/Chat/ChatClient.cs

410lines · modecode

1using Microsoft.TypeSpec.Generator.Customizations;
2using OpenAI.Telemetry;
3using System;
4using System.ClientModel;
5using System.ClientModel.Primitives;
6using System.Collections.Generic;
7using System.Diagnostics.CodeAnalysis;
8using System.Linq;
9using System.Threading;
10using System.Threading.Tasks;
11
12namespace OpenAI.Chat;
13
14// CUSTOM:
15// - Renamed.
16// - Suppressed constructor that takes endpoint parameter; endpoint is now a property in the options class.
17// - Suppressed methods that only take the options parameter.
18/// <summary> The service client for OpenAI chat operations. </summary>
19[CodeGenType("Chat")]
20[CodeGenSuppress("ChatClient", typeof(ClientPipeline), typeof(Uri))]
21[CodeGenSuppress("CompleteChat", typeof(ChatCompletionOptions), typeof(CancellationToken))]
22[CodeGenSuppress("CompleteChatAsync", typeof(ChatCompletionOptions), typeof(CancellationToken))]
23public partial class ChatClient
24{
25 private readonly string _model;
26 private readonly OpenTelemetrySource _telemetry;
27 private static readonly InternalChatCompletionStreamOptions s_includeUsageStreamOptions = new(includeUsage: true, patch: default);
28
29 // CUSTOM: Added as a convenience.
30 /// <summary> Initializes a new instance of <see cref="ChatClient"/>. </summary>
31 /// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
32 /// <param name="apiKey"> The API key to authenticate with the service. </param>
33 /// <exception cref="ArgumentNullException"> <paramref name="model"/> or <paramref name="apiKey"/> is null. </exception>
34 /// <exception cref="ArgumentException"> <paramref name="model"/> is an empty string, and was expected to be non-empty. </exception>
35 public ChatClient(string model, string apiKey) : this(model, new ApiKeyCredential(apiKey), new OpenAIClientOptions())
36 {
37 }
38
39 // CUSTOM:
40 // - Added `model` parameter.
41 // - Used a custom pipeline.
42 // - Demoted the endpoint parameter to be a property in the options class.
43 /// <summary> Initializes a new instance of <see cref="ChatClient"/>. </summary>
44 /// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
45 /// <param name="credential"> The <see cref="ApiKeyCredential"/> to authenticate with the service. </param>
46 /// <exception cref="ArgumentNullException"> <paramref name="model"/> or <paramref name="credential"/> is null. </exception>
47 /// <exception cref="ArgumentException"> <paramref name="model"/> is an empty string, and was expected to be non-empty. </exception>
48 public ChatClient(string model, ApiKeyCredential credential) : this(model, credential, new OpenAIClientOptions())
49 {
50 }
51
52 // CUSTOM:
53 // - Added `model` parameter.
54 // - Used a custom pipeline.
55 // - Demoted the endpoint parameter to be a property in the options class.
56 // - Added telemetry support.
57 /// <summary> Initializes a new instance of <see cref="ChatClient"/>. </summary>
58 /// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
59 /// <param name="credential"> The <see cref="ApiKeyCredential"/> to authenticate with the service. </param>
60 /// <param name="options"> The options to configure the client. </param>
61 /// <exception cref="ArgumentNullException"> <paramref name="model"/> or <paramref name="credential"/> is null. </exception>
62 /// <exception cref="ArgumentException"> <paramref name="model"/> is an empty string, and was expected to be non-empty. </exception>
63 public ChatClient(string model, ApiKeyCredential credential, OpenAIClientOptions options) : this(model, OpenAIClient.CreateApiKeyAuthenticationPolicy(credential), options)
64 {
65 }
66
67 // CUSTOM: Added as a convenience.
68 /// <summary> Initializes a new instance of <see cref="ChatClient"/>. </summary>
69 /// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
70 /// <param name="authenticationPolicy"> The authentication policy used to authenticate with the service. </param>
71 /// <exception cref="ArgumentNullException"> <paramref name="model"/> or <paramref name="authenticationPolicy"/> is null. </exception>
72 /// <exception cref="ArgumentException"> <paramref name="model"/> is an empty string, and was expected to be non-empty. </exception>
73 [Experimental("OPENAI001")]
74 public ChatClient(string model, AuthenticationPolicy authenticationPolicy) : this(model, authenticationPolicy, new OpenAIClientOptions())
75 {
76 }
77
78 // CUSTOM: Added as a convenience.
79 /// <summary> Initializes a new instance of <see cref="ChatClient"/>. </summary>
80 /// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
81 /// <param name="authenticationPolicy"> The authentication policy used to authenticate with the service. </param>
82 /// <param name="options"> The options to configure the client. </param>
83 /// <exception cref="ArgumentNullException"> <paramref name="model"/> or <paramref name="authenticationPolicy"/> is null. </exception>
84 /// <exception cref="ArgumentException"> <paramref name="model"/> is an empty string, and was expected to be non-empty. </exception>
85 [Experimental("OPENAI001")]
86 public ChatClient(string model, AuthenticationPolicy authenticationPolicy, OpenAIClientOptions options)
87 {
88 Argument.AssertNotNullOrEmpty(model, nameof(model));
89 Argument.AssertNotNull(authenticationPolicy, nameof(authenticationPolicy));
90 options ??= new OpenAIClientOptions();
91
92 _model = model;
93 Pipeline = OpenAIClient.CreatePipeline(authenticationPolicy, options);
94 _endpoint = OpenAIClient.GetEndpoint(options);
95 _telemetry = new OpenTelemetrySource(model, _endpoint);
96 }
97
98 // CUSTOM:
99 // - Added `model` parameter.
100 // - Used a custom pipeline.
101 // - Demoted the endpoint parameter to be a property in the options class.
102 // - Added telemetry support.
103 // - Made protected.
104 /// <summary> Initializes a new instance of <see cref="ChatClient"/>. </summary>
105 /// <param name="pipeline"> The HTTP pipeline to send and receive REST requests and responses. </param>
106 /// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
107 /// <param name="options"> The options to configure the client. </param>
108 /// <exception cref="ArgumentNullException"> <paramref name="pipeline"/> or <paramref name="model"/> is null. </exception>
109 /// <exception cref="ArgumentException"> <paramref name="model"/> is an empty string, and was expected to be non-empty. </exception>
110 protected internal ChatClient(ClientPipeline pipeline, string model, OpenAIClientOptions options)
111 {
112 Argument.AssertNotNull(pipeline, nameof(pipeline));
113 Argument.AssertNotNullOrEmpty(model, nameof(model));
114 options ??= new OpenAIClientOptions();
115
116 _model = model;
117 Pipeline = pipeline;
118 _endpoint = OpenAIClient.GetEndpoint(options);
119 _telemetry = new OpenTelemetrySource(model, _endpoint);
120 }
121
122 [Experimental("SCME0002")]
123 public ChatClient(ChatClientSettings settings)
124 : this(settings?.Model, AuthenticationPolicy.Create(settings), settings?.Options)
125 {
126 }
127
128 /// <summary>
129 /// Gets the name of the model used in requests sent to the service.
130 /// </summary>
131 [Experimental("OPENAI001")]
132 public string Model => _model;
133
134 /// <summary>
135 /// Gets the endpoint URI for the service.
136 /// </summary>
137 [Experimental("OPENAI001")]
138 public Uri Endpoint => _endpoint;
139
140 /// <summary> Generates a completion for the given chat. </summary>
141 /// <param name="messages"> The messages comprising the chat so far. </param>
142 /// <param name="options"> The options to configure the chat completion. </param>
143 /// <param name="cancellationToken"> A token that can be used to cancel this method call. </param>
144 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
145 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
146 public virtual Task<ClientResult<ChatCompletion>> CompleteChatAsync(IEnumerable<ChatMessage> messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default)
147 {
148 return CompleteChatAsync(messages, options, cancellationToken.ToRequestOptions() ?? new RequestOptions());
149 }
150
151 internal async Task<ClientResult<ChatCompletion>> CompleteChatAsync(IEnumerable<ChatMessage> messages, ChatCompletionOptions options, RequestOptions requestOptions)
152 {
153 Argument.AssertNotNullOrEmpty(messages, nameof(messages));
154 Argument.AssertNotNull(requestOptions, nameof(requestOptions));
155 if (requestOptions.BufferResponse is false)
156 {
157 throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'true' when calling 'CompleteChatAsync'.");
158 }
159
160 options ??= new();
161 var clonedOptions = CreateChatCompletionOptions(messages, options);
162 using OpenTelemetryScope scope = _telemetry.StartChatScope(clonedOptions);
163
164 try
165 {
166 using BinaryContent content = clonedOptions.ToBinaryContent();
167
168 ClientResult result = await CompleteChatAsync(content, requestOptions).ConfigureAwait(false);
169 ChatCompletion chatCompletion = (ChatCompletion)result;
170 scope?.RecordChatCompletion(chatCompletion);
171 return ClientResult.FromValue(chatCompletion, result.GetRawResponse());
172 }
173 catch (Exception ex)
174 {
175 scope?.RecordException(ex);
176 throw;
177 }
178 }
179
180 /// <summary> Generates a completion for the given chat. </summary>
181 /// <param name="messages"> The messages comprising the chat so far. </param>
182 /// <param name="options"> The options to configure the chat completion. </param>
183 /// <param name="cancellationToken"> A token that can be used to cancel this method call. </param>
184 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
185 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
186 public virtual ClientResult<ChatCompletion> CompleteChat(IEnumerable<ChatMessage> messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default)
187 {
188 Argument.AssertNotNullOrEmpty(messages, nameof(messages));
189
190 options ??= new();
191 var clonedOptions = CreateChatCompletionOptions(messages, options);
192 using OpenTelemetryScope scope = _telemetry.StartChatScope(clonedOptions);
193
194 try
195 {
196 using BinaryContent content = clonedOptions.ToBinaryContent();
197 ClientResult result = CompleteChat(content, cancellationToken.ToRequestOptions());
198 ChatCompletion chatCompletion = (ChatCompletion)result;
199
200 scope?.RecordChatCompletion(chatCompletion);
201 return ClientResult.FromValue(chatCompletion, result.GetRawResponse());
202 }
203 catch (Exception ex)
204 {
205 scope?.RecordException(ex);
206 throw;
207 }
208 }
209
210 /// <summary> Generates a completion for the given chat. </summary>
211 /// <param name="messages"> The messages comprising the chat so far. </param>
212 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
213 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
214 public virtual async Task<ClientResult<ChatCompletion>> CompleteChatAsync(params ChatMessage[] messages)
215 => await CompleteChatAsync(messages, default(ChatCompletionOptions)).ConfigureAwait(false);
216
217 /// <summary> Generates a completion for the given chat. </summary>
218 /// <param name="messages"> The messages comprising the chat so far. </param>
219 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
220 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
221 public virtual ClientResult<ChatCompletion> CompleteChat(params ChatMessage[] messages)
222 => CompleteChat(messages, default(ChatCompletionOptions));
223
224 /// <summary>
225 /// Generates a completion for the given chat. The completion is streamed back token by token as it is being
226 /// generated by the model instead of waiting for it to be finished first.
227 /// </summary>
228 /// <remarks>
229 /// <see cref="AsyncCollectionResult{T}"/> implements the <see cref="IAsyncEnumerable{T}"/> interface and can be
230 /// enumerated over using the <c>await foreach</c> pattern.
231 /// </remarks>
232 /// <param name="messages"> The messages comprising the chat so far. </param>
233 /// <param name="options"> The options to configure the chat completion. </param>
234 /// <param name="cancellationToken"> A token that can be used to cancel this method call. </param>
235 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
236 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
237 public virtual AsyncCollectionResult<StreamingChatCompletionUpdate> CompleteChatStreamingAsync(IEnumerable<ChatMessage> messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default)
238 {
239 return CompleteChatStreamingAsync(messages, options, cancellationToken.ToRequestOptions(streaming: true));
240 }
241
242 internal AsyncCollectionResult<StreamingChatCompletionUpdate> CompleteChatStreamingAsync(IEnumerable<ChatMessage> messages, ChatCompletionOptions options, RequestOptions requestOptions)
243 {
244 Argument.AssertNotNull(messages, nameof(messages));
245 Argument.AssertNotNull(requestOptions, nameof(requestOptions));
246 if (requestOptions.BufferResponse is true)
247 {
248 throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'false' when calling 'CompleteChatStreamingAsync'.");
249 }
250
251 options ??= new();
252 var clonedOptions = CreateChatCompletionOptions(messages, options, stream: true);
253
254 using BinaryContent content = clonedOptions.ToBinaryContent();
255 return new AsyncSseUpdateCollection<StreamingChatCompletionUpdate>(
256 async () => await CompleteChatAsync(content, requestOptions).ConfigureAwait(false),
257 StreamingChatCompletionUpdate.DeserializeStreamingChatCompletionUpdate,
258 requestOptions.CancellationToken);
259 }
260
261 /// <summary>
262 /// Generates a completion for the given chat. The completion is streamed back token by token as it is being
263 /// generated by the model instead of waiting for it to be finished first.
264 /// </summary>
265 /// <remarks>
266 /// <see cref="CollectionResult{T}"/> implements the <see cref="IEnumerable{T}"/> interface and can be
267 /// enumerated over using the <c>await foreach</c> pattern.
268 /// </remarks>
269 /// <param name="messages"> The messages comprising the chat so far. </param>
270 /// <param name="options"> The options to configure the chat completion. </param>
271 /// <param name="cancellationToken"> A token that can be used to cancel this method call. </param>
272 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
273 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
274 public virtual CollectionResult<StreamingChatCompletionUpdate> CompleteChatStreaming(IEnumerable<ChatMessage> messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default)
275 {
276 Argument.AssertNotNull(messages, nameof(messages));
277
278 options ??= new();
279 var clonedOptions = CreateChatCompletionOptions(messages, options, stream: true);
280
281 using BinaryContent content = clonedOptions.ToBinaryContent();
282 return new SseUpdateCollection<StreamingChatCompletionUpdate>(
283 () => CompleteChat(content, cancellationToken.ToRequestOptions(streaming: true)),
284 StreamingChatCompletionUpdate.DeserializeStreamingChatCompletionUpdate,
285 cancellationToken);
286 }
287
288 /// <summary>
289 /// Generates a completion for the given chat. The completion is streamed back token by token as it is being
290 /// generated by the model instead of waiting for it to be finished first.
291 /// </summary>
292 /// <remarks>
293 /// <see cref="AsyncCollectionResult{T}"/> implements the <see cref="IAsyncEnumerable{T}"/> interface and can be
294 /// enumerated over using the <c>await foreach</c> pattern.
295 /// </remarks>
296 /// <param name="messages"> The messages comprising the chat so far. </param>
297 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
298 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
299 public virtual AsyncCollectionResult<StreamingChatCompletionUpdate> CompleteChatStreamingAsync(params ChatMessage[] messages)
300 => CompleteChatStreamingAsync(messages, default(ChatCompletionOptions));
301
302 /// <summary>
303 /// Generates a completion for the given chat. The completion is streamed back token by token as it is being
304 /// generated by the model instead of waiting for it to be finished first.
305 /// </summary>
306 /// <remarks>
307 /// <see cref="CollectionResult{T}"/> implements the <see cref="IEnumerable{T}"/> interface and can be
308 /// enumerated over using the <c>await foreach</c> pattern.
309 /// </remarks>
310 /// <param name="messages"> The messages comprising the chat so far. </param>
311 /// <exception cref="ArgumentNullException"> <paramref name="messages"/> is null. </exception>
312 /// <exception cref="ArgumentException"> <paramref name="messages"/> is an empty collection, and was expected to be non-empty. </exception>
313 public virtual CollectionResult<StreamingChatCompletionUpdate> CompleteChatStreaming(params ChatMessage[] messages)
314 => CompleteChatStreaming(messages, default(ChatCompletionOptions));
315
316 // CUSTOM:
317 // - Added Experimental attribute.
318 // - Call FromClientResult.
319 [Experimental("OPENAI001")]
320 public virtual async Task<ClientResult<ChatCompletion>> GetChatCompletionAsync(string completionId, CancellationToken cancellationToken = default)
321 {
322 Argument.AssertNotNullOrEmpty(completionId, nameof(completionId));
323
324 ClientResult result = await GetChatCompletionAsync(completionId, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
325 return ClientResult.FromValue((ChatCompletion)result, result.GetRawResponse());
326 }
327
328 // CUSTOM:
329 // - Added Experimental attribute.
330 // - Call FromClientResult.
331 [Experimental("OPENAI001")]
332 public virtual ClientResult<ChatCompletion> GetChatCompletion(string completionId, CancellationToken cancellationToken = default)
333 {
334 Argument.AssertNotNullOrEmpty(completionId, nameof(completionId));
335
336 ClientResult result = GetChatCompletion(completionId, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
337 return ClientResult.FromValue((ChatCompletion)result, result.GetRawResponse());
338 }
339
340 // CUSTOM:
341 // - Call FromClientResult.
342 [Experimental("OPENAI001")]
343 public virtual ClientResult<ChatCompletion> UpdateChatCompletion(string completionId, IDictionary<string, string> metadata, CancellationToken cancellationToken = default)
344 {
345 Argument.AssertNotNullOrEmpty(completionId, nameof(completionId));
346 Argument.AssertNotNull(metadata, nameof(metadata));
347
348 InternalUpdateChatCompletionRequest spreadModel = new InternalUpdateChatCompletionRequest(metadata, null);
349 ClientResult result = this.UpdateChatCompletion(completionId, spreadModel, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
350 return ClientResult.FromValue((ChatCompletion)result, result.GetRawResponse());
351 }
352
353 // CUSTOM:
354 // - Call FromClientResult.
355 [Experimental("OPENAI001")]
356 public virtual async Task<ClientResult<ChatCompletion>> UpdateChatCompletionAsync(string completionId, IDictionary<string, string> metadata, CancellationToken cancellationToken = default)
357 {
358 Argument.AssertNotNullOrEmpty(completionId, nameof(completionId));
359 Argument.AssertNotNull(metadata, nameof(metadata));
360
361 InternalUpdateChatCompletionRequest spreadModel = new InternalUpdateChatCompletionRequest(metadata, null);
362 ClientResult result = await this.UpdateChatCompletionAsync(completionId, spreadModel, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
363 return ClientResult.FromValue((ChatCompletion)result, result.GetRawResponse());
364 }
365
366 // CUSTOM:
367 // - Added Experimental attribute.
368 // - Call FromClientResult.
369 [Experimental("OPENAI001")]
370 public virtual async Task<ClientResult<ChatCompletionDeletionResult>> DeleteChatCompletionAsync(string completionId, CancellationToken cancellationToken = default)
371 {
372 Argument.AssertNotNullOrEmpty(completionId, nameof(completionId));
373
374 ClientResult result = await DeleteChatCompletionAsync(completionId, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false);
375 return ClientResult.FromValue((ChatCompletionDeletionResult)result, result.GetRawResponse());
376 }
377
378 // CUSTOM:
379 // - Added Experimental attribute.
380 // - Call FromClientResult.
381 [Experimental("OPENAI001")]
382 public virtual ClientResult<ChatCompletionDeletionResult> DeleteChatCompletion(string completionId, CancellationToken cancellationToken = default)
383 {
384 Argument.AssertNotNullOrEmpty(completionId, nameof(completionId));
385
386 ClientResult result = DeleteChatCompletion(completionId, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null);
387 return ClientResult.FromValue((ChatCompletionDeletionResult)result, result.GetRawResponse());
388 }
389
390 private ChatCompletionOptions CreateChatCompletionOptions(IEnumerable<ChatMessage> messages, ChatCompletionOptions options, bool stream = false)
391 {
392 var clonedOptions = options.Clone();
393 foreach (var message in messages)
394 {
395 clonedOptions.Messages.Add(message);
396 }
397 clonedOptions.Model ??= _model;
398 if (stream)
399 {
400 clonedOptions.Stream = true;
401 clonedOptions.StreamOptions = s_includeUsageStreamOptions;
402 }
403 else
404 {
405 clonedOptions.Stream = null;
406 clonedOptions.StreamOptions = null;
407 }
408 return clonedOptions;
409 }
410}