diff --git a/OpenAI-DotNet-Tests/TestFixture_02_Completions.cs b/OpenAI-DotNet-Tests/TestFixture_02_Completions.cs index e3a9cf6d..7a53c1a7 100644 --- a/OpenAI-DotNet-Tests/TestFixture_02_Completions.cs +++ b/OpenAI-DotNet-Tests/TestFixture_02_Completions.cs @@ -10,24 +10,24 @@ namespace OpenAI.Tests { internal class TestFixture_02_Completions { - private readonly string prompts = "One Two Three Four Five Six Seven Eight Nine One Two Three Four Five Six Seven Eight"; + private const string CompletionPrompts = "One Two Three Four Five Six Seven Eight Nine One Two Three Four Five Six Seven Eight"; [Test] public async Task Test_01_GetBasicCompletion() { var api = new OpenAIClient(OpenAIAuthentication.LoadFromEnv()); Assert.IsNotNull(api.CompletionsEndpoint); - - var result = await api.CompletionsEndpoint.CreateCompletionAsync(prompts, temperature: 0.1, maxTokens: 5, numOutputs: 5, model: Model.Davinci); + var result = await api.CompletionsEndpoint.CreateCompletionAsync( + CompletionPrompts, + temperature: 0.1, + maxTokens: 5, + numOutputs: 5, + model: Model.Davinci); Assert.IsNotNull(result); Assert.NotNull(result.Completions); Assert.NotZero(result.Completions.Count); Assert.That(result.Completions.Any(c => c.Text.Trim().ToLower().StartsWith("nine"))); - - foreach (var choice in result.Completions) - { - Console.WriteLine(choice); - } + Console.WriteLine(result); } [Test] @@ -35,7 +35,6 @@ public async Task Test_02_GetStreamingCompletion() { var api = new OpenAIClient(OpenAIAuthentication.LoadFromEnv()); Assert.IsNotNull(api.CompletionsEndpoint); - var allCompletions = new List(); await api.CompletionsEndpoint.StreamCompletionAsync(result => @@ -44,13 +43,10 @@ public async Task Test_02_GetStreamingCompletion() Assert.NotNull(result.Completions); Assert.NotZero(result.Completions.Count); allCompletions.AddRange(result.Completions); + }, CompletionPrompts, temperature: 0.1, maxTokens: 5, numOutputs: 5); - foreach (var choice in result.Completions) - { - Console.WriteLine(choice); - } - }, prompts, temperature: 0.1, maxTokens: 5, numOutputs: 5, model: Model.Davinci); Assert.That(allCompletions.Any(c => c.Text.Trim().ToLower().StartsWith("nine"))); + Console.WriteLine(allCompletions.FirstOrDefault()); } [Test] @@ -58,19 +54,23 @@ public async Task Test_03_GetStreamingEnumerableCompletion() { var api = new OpenAIClient(OpenAIAuthentication.LoadFromEnv()); Assert.IsNotNull(api.CompletionsEndpoint); - var allCompletions = new List(); - await foreach (var result in api.CompletionsEndpoint.StreamCompletionEnumerableAsync(prompts, temperature: 0.1, maxTokens: 5, numOutputs: 5, model: Model.Davinci)) + await foreach (var result in api.CompletionsEndpoint.StreamCompletionEnumerableAsync( + CompletionPrompts, + temperature: 0.1, + maxTokens: 5, + numOutputs: 5, + model: Model.Davinci)) { Assert.IsNotNull(result); Assert.NotNull(result.Completions); Assert.NotZero(result.Completions.Count); - Console.WriteLine(result); allCompletions.AddRange(result.Completions); } Assert.That(allCompletions.Any(c => c.Text.Trim().ToLower().StartsWith("nine"))); + Console.WriteLine(allCompletions.FirstOrDefault()); } } } diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs index 33462cdc..2363f5b5 100644 --- a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs +++ b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs @@ -28,5 +28,57 @@ public async Task Test_1_GetChatCompletion() Assert.NotZero(result.Choices.Count); Console.WriteLine(result.FirstChoice); } + + [Test] + public async Task Test_2_GetChatStreamingCompletion() + { + var api = new OpenAIClient(OpenAIAuthentication.LoadFromEnv()); + Assert.IsNotNull(api.ChatEndpoint); + var chatPrompts = new List + { + new ChatPrompt("system", "You are a helpful assistant."), + new ChatPrompt("user", "Who won the world series in 2020?"), + new ChatPrompt("assistant", "The Los Angeles Dodgers won the World Series in 2020."), + new ChatPrompt("user", "Where was it played?"), + }; + var chatRequest = new ChatRequest(chatPrompts, Model.GPT3_5_Turbo); + var allContent = new List(); + + await api.ChatEndpoint.StreamCompletionAsync(chatRequest, result => + { + Assert.IsNotNull(result); + Assert.NotNull(result.Choices); + Assert.NotZero(result.Choices.Count); + allContent.Add(result.FirstChoice); + }); + + Console.WriteLine(string.Join("", allContent)); + } + + [Test] + public async Task Test_3_GetChatStreamingCompletionEnumerableAsync() + { + var api = new OpenAIClient(OpenAIAuthentication.LoadFromEnv()); + Assert.IsNotNull(api.ChatEndpoint); + var chatPrompts = new List + { + new ChatPrompt("system", "You are a helpful assistant."), + new ChatPrompt("user", "Who won the world series in 2020?"), + new ChatPrompt("assistant", "The Los Angeles Dodgers won the World Series in 2020."), + new ChatPrompt("user", "Where was it played?"), + }; + var chatRequest = new ChatRequest(chatPrompts, Model.GPT3_5_Turbo); + var allContent = new List(); + + await foreach (var result in api.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) + { + Assert.IsNotNull(result); + Assert.NotNull(result.Choices); + Assert.NotZero(result.Choices.Count); + allContent.Add(result.FirstChoice); + } + + Console.WriteLine(string.Join("", allContent)); + } } } \ No newline at end of file diff --git a/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs b/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs index 6f93f4b5..bedd1d12 100644 --- a/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs +++ b/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs @@ -102,11 +102,11 @@ public sealed class AudioTranscriptionRequest AudioName = audioName; - Model = model ?? new Model("whisper-1"); + Model = model ?? Models.Model.Whisper1; if (!Model.Contains("whisper")) { - throw new ArgumentException(nameof(model), $"{Model} is not supported."); + throw new ArgumentException($"{Model} is not supported", nameof(model)); } Prompt = prompt; diff --git a/OpenAI-DotNet/Audio/AudioTranslationRequest.cs b/OpenAI-DotNet/Audio/AudioTranslationRequest.cs index 3af2f495..3c2f2be6 100644 --- a/OpenAI-DotNet/Audio/AudioTranslationRequest.cs +++ b/OpenAI-DotNet/Audio/AudioTranslationRequest.cs @@ -82,11 +82,11 @@ public sealed class AudioTranslationRequest AudioName = audioName; - Model = model ?? new Model("whisper-1"); + Model = model ?? Models.Model.Whisper1; if (!Model.Contains("whisper")) { - throw new ArgumentException(nameof(model), $"{Model} is not supported."); + throw new ArgumentException($"{Model} is not supported", nameof(model)); } Prompt = prompt; diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs index fe0338a6..b388bb55 100644 --- a/OpenAI-DotNet/Chat/ChatEndpoint.cs +++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -22,11 +26,92 @@ public async Task GetCompletionAsync(ChatRequest chatRequest, Canc { var json = JsonSerializer.Serialize(chatRequest, Api.JsonSerializationOptions); var payload = json.ToJsonStringContent(); - var result = await Api.Client.PostAsync($"{GetEndpoint()}/completions", payload, cancellationToken); - var resultAsString = await result.ReadAsStringAsync(cancellationToken); - return JsonSerializer.Deserialize(resultAsString, Api.JsonSerializationOptions); + var response = await Api.Client.PostAsync($"{GetEndpoint()}/completions", payload, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + return response.DeserializeResponse(responseAsString, Api.JsonSerializationOptions); } - // TODO Streaming endpoints + + /// + /// Created a completion for the chat message and stream the results to the as they come in. + /// + /// The chat request which contains the message content. + /// An action to be called as each new result arrives. + /// Optional, . + /// . + /// Raised when the HTTP request fails + public async Task StreamCompletionAsync(ChatRequest chatRequest, Action resultHandler, CancellationToken cancellationToken = default) + { + chatRequest.Stream = true; + var jsonContent = JsonSerializer.Serialize(chatRequest, Api.JsonSerializationOptions); + using var request = new HttpRequestMessage(HttpMethod.Post, $"{GetEndpoint()}/completions") + { + Content = jsonContent.ToJsonStringContent() + }; + var response = await Api.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await response.CheckResponseAsync(cancellationToken); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(stream); + + while (await reader.ReadLineAsync() is { } line) + { + if (line.StartsWith("data: ")) + { + line = line["data: ".Length..]; + } + + if (line == "[DONE]") + { + return; + } + + if (!string.IsNullOrWhiteSpace(line)) + { + resultHandler(response.DeserializeResponse(line.Trim(), Api.JsonSerializationOptions)); + } + } + } + + /// + /// Created a completion for the chat message and stream the results as they come in.
+ /// If you are not using C# 8 supporting IAsyncEnumerable{T} or if you are using the .NET Framework, + /// you may need to use instead. + ///
+ /// The chat request which contains the message content. + /// Optional, . + /// . + /// Raised when the HTTP request fails + public async IAsyncEnumerable StreamCompletionEnumerableAsync(ChatRequest chatRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + chatRequest.Stream = true; + var jsonContent = JsonSerializer.Serialize(chatRequest, Api.JsonSerializationOptions); + using var request = new HttpRequestMessage(HttpMethod.Post, $"{GetEndpoint()}/completions") + { + Content = jsonContent.ToJsonStringContent() + }; + var response = await Api.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await response.CheckResponseAsync(cancellationToken); + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(stream); + + while (await reader.ReadLineAsync() is { } line && + !cancellationToken.IsCancellationRequested) + { + if (line.StartsWith("data: ")) + { + line = line["data: ".Length..]; + } + + if (line == "[DONE]") + { + yield break; + } + + if (!string.IsNullOrWhiteSpace(line)) + { + yield return response.DeserializeResponse(line.Trim(), Api.JsonSerializationOptions); + } + } + } } } diff --git a/OpenAI-DotNet/Chat/ChatRequest.cs b/OpenAI-DotNet/Chat/ChatRequest.cs index 710e897f..1692b370 100644 --- a/OpenAI-DotNet/Chat/ChatRequest.cs +++ b/OpenAI-DotNet/Chat/ChatRequest.cs @@ -9,6 +9,62 @@ namespace OpenAI.Chat { public sealed class ChatRequest { + /// + /// Constructor. + /// + /// + /// + /// ID of the model to use. Currently, only gpt-3.5-turbo and gpt-3.5-turbo-0301 are supported. + /// + /// + /// What sampling temperature to use, between 0 and 2. + /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will + /// make it more focused and deterministic. + /// We generally recommend altering this or top_p but not both.
+ /// Defaults to 1 + /// + /// + /// An alternative to sampling with temperature, called nucleus sampling, + /// where the model considers the results of the tokens with top_p probability mass. + /// So 0.1 means only the tokens comprising the top 10% probability mass are considered. + /// We generally recommend altering this or temperature but not both.
+ /// Defaults to 1 + /// + /// + /// How many chat completion choices to generate for each input message.
+ /// Defaults to 1 + /// + /// + /// Up to 4 sequences where the API will stop generating further tokens. + /// + /// + /// The maximum number of tokens allowed for the generated answer. + /// By default, the number of tokens the model can return will be (4096 - prompt tokens). + /// + /// + /// Number between -2.0 and 2.0. + /// Positive values penalize new tokens based on whether they appear in the text so far, + /// increasing the model's likelihood to talk about new topics.
+ /// Defaults to 0 + /// + /// + /// Number between -2.0 and 2.0. + /// Positive values penalize new tokens based on their existing frequency in the text so far, + /// decreasing the model's likelihood to repeat the same line verbatim.
+ /// Defaults to 0 + /// + /// + /// Modify the likelihood of specified tokens appearing in the completion. + /// Accepts a json object that maps tokens(specified by their token ID in the tokenizer) + /// to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits + /// generated by the model prior to sampling.The exact effect will vary per model, but values between + /// -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result + /// in a ban or exclusive selection of the relevant token.
+ /// Defaults to null + /// + /// + /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. + /// public ChatRequest( IEnumerable messages, Model model = null, @@ -22,12 +78,11 @@ public sealed class ChatRequest Dictionary logitBias = null, string user = null) { - const string defaultModel = "gpt-3.5-turbo"; Model = model ?? Models.Model.GPT3_5_Turbo; - if (!Model.Contains(defaultModel)) + if (!Model.Contains("turbo")) { - throw new ArgumentException(nameof(model), $"{Model} not supported"); + throw new ArgumentException($"{Model} is not supported", nameof(model)); } Messages = messages?.ToList(); @@ -126,7 +181,8 @@ public sealed class ChatRequest [JsonPropertyName("frequency_penalty")] public double? FrequencyPenalty { get; } - /// Modify the likelihood of specified tokens appearing in the completion. + /// + /// Modify the likelihood of specified tokens appearing in the completion. /// Accepts a json object that maps tokens(specified by their token ID in the tokenizer) /// to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits /// generated by the model prior to sampling.The exact effect will vary per model, but values between @@ -135,7 +191,7 @@ public sealed class ChatRequest /// Defaults to null /// [JsonPropertyName("logit_bias")] - public IReadOnlyDictionary LogitBias { get; set; } + public IReadOnlyDictionary LogitBias { get; } /// /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. diff --git a/OpenAI-DotNet/Chat/ChatResponse.cs b/OpenAI-DotNet/Chat/ChatResponse.cs index 0d263763..e2c54cfd 100644 --- a/OpenAI-DotNet/Chat/ChatResponse.cs +++ b/OpenAI-DotNet/Chat/ChatResponse.cs @@ -5,25 +5,31 @@ namespace OpenAI.Chat { - public sealed class ChatResponse + public sealed class ChatResponse : BaseResponse { + [JsonInclude] [JsonPropertyName("id")] - public string Id { get; set; } + public string Id { get; private set; } + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; set; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("created")] - public int Created { get; set; } + public int Created { get; private set; } + [JsonInclude] [JsonPropertyName("model")] - public string Model { get; set; } + public string Model { get; private set; } + [JsonInclude] [JsonPropertyName("usage")] - public Usage Usage { get; set; } + public Usage Usage { get; private set; } + [JsonInclude] [JsonPropertyName("choices")] - public IReadOnlyList Choices { get; set; } + public IReadOnlyList Choices { get; private set; } [JsonIgnore] public Choice FirstChoice => Choices.FirstOrDefault(); diff --git a/OpenAI-DotNet/Chat/Choice.cs b/OpenAI-DotNet/Chat/Choice.cs index 15528639..02ba5328 100644 --- a/OpenAI-DotNet/Chat/Choice.cs +++ b/OpenAI-DotNet/Chat/Choice.cs @@ -4,27 +4,23 @@ namespace OpenAI.Chat { public sealed class Choice { - [JsonConstructor] - public Choice( - Message message, - string finishReason, - int index) - { - Message = message; - FinishReason = finishReason; - Index = index; - } - + [JsonInclude] [JsonPropertyName("message")] - public Message Message { get; } + public Message Message { get; private set; } + + [JsonInclude] + [JsonPropertyName("delta")] + public Delta Delta { get; private set; } + [JsonInclude] [JsonPropertyName("finish_reason")] - public string FinishReason { get; } + public string FinishReason { get; private set; } + [JsonInclude] [JsonPropertyName("index")] - public int Index { get; } + public int Index { get; private set; } - public override string ToString() => Message.ToString(); + public override string ToString() => Message?.ToString() ?? Delta.Content; public static implicit operator string(Choice choice) => choice.ToString(); } diff --git a/OpenAI-DotNet/Chat/Delta.cs b/OpenAI-DotNet/Chat/Delta.cs new file mode 100644 index 00000000..a87a9858 --- /dev/null +++ b/OpenAI-DotNet/Chat/Delta.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace OpenAI.Chat +{ + public sealed class Delta + { + [JsonInclude] + [JsonPropertyName("role")] + public string Role { get; private set; } + + [JsonInclude] + [JsonPropertyName("content")] + public string Content { get; private set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Chat/Message.cs b/OpenAI-DotNet/Chat/Message.cs index 22fb4f69..f5e9b6db 100644 --- a/OpenAI-DotNet/Chat/Message.cs +++ b/OpenAI-DotNet/Chat/Message.cs @@ -4,20 +4,13 @@ namespace OpenAI.Chat { public sealed class Message { - [JsonConstructor] - public Message( - string role, - string content) - { - Role = role; - Content = content; - } - + [JsonInclude] [JsonPropertyName("role")] - public string Role { get; } + public string Role { get; private set; } + [JsonInclude] [JsonPropertyName("content")] - public string Content { get; } + public string Content { get; private set; } public override string ToString() => Content; diff --git a/OpenAI-DotNet/Completions/Choice.cs b/OpenAI-DotNet/Completions/Choice.cs index 7888ccd2..b661f670 100644 --- a/OpenAI-DotNet/Completions/Choice.cs +++ b/OpenAI-DotNet/Completions/Choice.cs @@ -7,42 +7,33 @@ namespace OpenAI.Completions /// public sealed class Choice { - [JsonConstructor] - public Choice( - string text, - int index, - LogProbabilities logProbabilities, - string finishReason) - { - Text = text; - Index = index; - LogProbabilities = logProbabilities; - FinishReason = finishReason; - } - /// /// The main text of the completion /// + [JsonInclude] [JsonPropertyName("text")] - public string Text { get; } + public string Text { get; private set; } /// /// If multiple completion choices we returned, this is the index withing the various choices /// + [JsonInclude] [JsonPropertyName("index")] - public int Index { get; } + public int Index { get; private set; } /// /// If the request specified , this contains the list of the most likely tokens. /// + [JsonInclude] [JsonPropertyName("logprobs")] - public LogProbabilities LogProbabilities { get; } + public LogProbabilities LogProbabilities { get; private set; } /// /// If this is the last segment of the completion result, this specifies why the completion has ended. /// + [JsonInclude] [JsonPropertyName("finish_reason")] - public string FinishReason { get; } + public string FinishReason { get; private set; } /// /// Gets the main text of this completion diff --git a/OpenAI-DotNet/Completions/CompletionRequest.cs b/OpenAI-DotNet/Completions/CompletionRequest.cs index d281dc74..f29439a1 100644 --- a/OpenAI-DotNet/Completions/CompletionRequest.cs +++ b/OpenAI-DotNet/Completions/CompletionRequest.cs @@ -200,7 +200,7 @@ public CompletionRequest(CompletionRequest basedOn) return; } - Model = basedOn.Model; + Model = basedOn.Model ?? DefaultCompletionRequestArgs?.Model ?? Models.Model.Davinci; Prompts = basedOn.Prompts; Suffix = basedOn.Suffix ?? DefaultCompletionRequestArgs?.Suffix; MaxTokens = basedOn.MaxTokens ?? DefaultCompletionRequestArgs?.MaxTokens; @@ -278,7 +278,17 @@ public CompletionRequest(CompletionRequest basedOn) throw new ArgumentNullException($"Missing required {nameof(prompt)}(s)"); } - Model = model ?? DefaultCompletionRequestArgs?.Model; + Model = model ?? DefaultCompletionRequestArgs?.Model ?? Models.Model.Davinci; + + if (!Model.Contains("davinci") && + !Model.Contains("curie") && + !Model.Contains("cushman") && + !Model.Contains("babbage") && + !Model.Contains("ada")) + { + throw new ArgumentException($"{Model} is not supported", nameof(model)); + } + Suffix = suffix ?? DefaultCompletionRequestArgs?.Suffix; MaxTokens = maxTokens ?? DefaultCompletionRequestArgs?.MaxTokens; Temperature = temperature ?? DefaultCompletionRequestArgs?.Temperature; diff --git a/OpenAI-DotNet/Completions/CompletionResult.cs b/OpenAI-DotNet/Completions/CompletionResult.cs index 861ef5a1..891b7ae8 100644 --- a/OpenAI-DotNet/Completions/CompletionResult.cs +++ b/OpenAI-DotNet/Completions/CompletionResult.cs @@ -9,35 +9,23 @@ namespace OpenAI.Completions /// public sealed class CompletionResult : BaseResponse { - [JsonConstructor] - public CompletionResult( - string id, - string @object, - int createdUnixTime, - string model, - IReadOnlyList completions) - { - Id = id; - Object = @object; - CreatedUnixTime = createdUnixTime; - Model = model; - Completions = completions; - } - /// /// The identifier of the result, which may be used during troubleshooting /// + [JsonInclude] [JsonPropertyName("id")] - public string Id { get; } + public string Id { get; private set; } + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } /// /// The time when the result was generated in unix epoch format /// + [JsonInclude] [JsonPropertyName("created")] - public int CreatedUnixTime { get; } + public int CreatedUnixTime { get; private set; } /// /// The time when the result was generated. @@ -45,14 +33,16 @@ public sealed class CompletionResult : BaseResponse [JsonIgnore] public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + [JsonInclude] [JsonPropertyName("model")] - public string Model { get; } + public string Model { get; private set; } /// /// The completions returned by the API. Depending on your request, there may be 1 or many choices. /// + [JsonInclude] [JsonPropertyName("choices")] - public IReadOnlyList Completions { get; } + public IReadOnlyList Completions { get; private set; } /// /// Gets the text of the first completion, representing the main result diff --git a/OpenAI-DotNet/Completions/CompletionsEndpoint.cs b/OpenAI-DotNet/Completions/CompletionsEndpoint.cs index afff5437..3c1cb77b 100644 --- a/OpenAI-DotNet/Completions/CompletionsEndpoint.cs +++ b/OpenAI-DotNet/Completions/CompletionsEndpoint.cs @@ -57,7 +57,7 @@ protected override string GetEndpoint() /// One or more sequences where the API will stop generating further tokens. /// The returned text will not contain the stop sequence. /// Optional, to use when calling the API. - /// Defaults to . + /// Defaults to . /// Optional, . /// Asynchronously returns the completion result. /// Look in its property for the completions. @@ -78,7 +78,7 @@ protected override string GetEndpoint() CancellationToken cancellationToken = default) { var request = new CompletionRequest( - model ?? Api.DefaultModel, + model ?? Model.Davinci, prompt, prompts, suffix, @@ -110,7 +110,7 @@ public async Task CreateCompletionAsync(CompletionRequest comp var jsonContent = JsonSerializer.Serialize(completionRequest, Api.JsonSerializationOptions); var response = await Api.Client.PostAsync(GetEndpoint(), jsonContent.ToJsonStringContent(), cancellationToken).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - return DeserializeResult(response, responseAsString); + return response.DeserializeResponse(responseAsString, Api.JsonSerializationOptions); } #endregion Non-Streaming @@ -147,7 +147,7 @@ public async Task CreateCompletionAsync(CompletionRequest comp /// One or more sequences where the API will stop generating further tokens. /// The returned text will not contain the stop sequence. /// Optional, to use when calling the API. - /// Defaults to . + /// Defaults to . /// Optional, . /// An async enumerable with each of the results as they come in. /// See the C# docs @@ -170,7 +170,7 @@ public async Task CreateCompletionAsync(CompletionRequest comp CancellationToken cancellationToken = default) { var request = new CompletionRequest( - model ?? Api.DefaultModel, + model ?? Model.Davinci, prompt, prompts, suffix, @@ -222,13 +222,13 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act if (!string.IsNullOrWhiteSpace(line)) { - resultHandler(DeserializeResult(response, line.Trim())); + resultHandler(response.DeserializeResponse(line.Trim(), Api.JsonSerializationOptions)); } } } /// - /// Ask the API to complete the prompt(s) using the specified parameters. + /// Ask the API to complete the prompt(s) using the specified request, and stream the results as they come in.
/// If you are not using C# 8 supporting IAsyncEnumerable{T} or if you are using the .NET Framework, /// you may need to use instead. ///
@@ -257,7 +257,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act /// One or more sequences where the API will stop generating further tokens. /// The returned text will not contain the stop sequence. /// Optional, to use when calling the API. - /// Defaults to . + /// Defaults to . /// Optional, . /// An async enumerable with each of the results as they come in. /// See the C# docs @@ -279,7 +279,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act CancellationToken cancellationToken = default) { var request = new CompletionRequest( - model ?? Api.DefaultModel, + model ?? Model.Davinci, prompt, prompts, suffix, @@ -297,7 +297,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act } /// - /// Ask the API to complete the prompt(s) using the specified request, and stream the results as they come in. + /// Ask the API to complete the prompt(s) using the specified request, and stream the results as they come in.
/// If you are not using C# 8 supporting IAsyncEnumerable{T} or if you are using the .NET Framework, /// you may need to use instead. ///
@@ -335,25 +335,11 @@ public async IAsyncEnumerable StreamCompletionEnumerableAsync( if (!string.IsNullOrWhiteSpace(line)) { - yield return DeserializeResult(response, line.Trim()); + yield return response.DeserializeResponse(line.Trim(), Api.JsonSerializationOptions); } } } #endregion Streaming - - private CompletionResult DeserializeResult(HttpResponseMessage response, string json) - { - var result = JsonSerializer.Deserialize(json, Api.JsonSerializationOptions); - - if (result?.Completions == null || result.Completions.Count == 0) - { - throw new HttpRequestException($"{nameof(DeserializeResult)} no completions! HTTP status code: {response.StatusCode}. Response body: {json}"); - } - - result.SetResponseData(response.Headers); - - return result; - } } } diff --git a/OpenAI-DotNet/Completions/LogProbabilities.cs b/OpenAI-DotNet/Completions/LogProbabilities.cs index cfe25d9d..684555e0 100644 --- a/OpenAI-DotNet/Completions/LogProbabilities.cs +++ b/OpenAI-DotNet/Completions/LogProbabilities.cs @@ -8,29 +8,20 @@ namespace OpenAI.Completions ///
public sealed class LogProbabilities { - [JsonConstructor] - public LogProbabilities( - IReadOnlyList tokens, - IReadOnlyList tokenLogProbabilities, - IList> topLogProbabilities, - IReadOnlyList textOffsets) - { - Tokens = tokens; - TokenLogProbabilities = tokenLogProbabilities; - TopLogProbabilities = topLogProbabilities; - TextOffsets = textOffsets; - } - + [JsonInclude] [JsonPropertyName("tokens")] - public IReadOnlyList Tokens { get; } + public IReadOnlyList Tokens { get; private set; } + [JsonInclude] [JsonPropertyName("token_logprobs")] - public IReadOnlyList TokenLogProbabilities { get; } + public IReadOnlyList TokenLogProbabilities { get; private set; } + [JsonInclude] [JsonPropertyName("top_logprobs")] - public IList> TopLogProbabilities { get; } + public IList> TopLogProbabilities { get; private set; } + [JsonInclude] [JsonPropertyName("text_offset")] - public IReadOnlyList TextOffsets { get; } + public IReadOnlyList TextOffsets { get; private set; } } } diff --git a/OpenAI-DotNet/Edits/Choice.cs b/OpenAI-DotNet/Edits/Choice.cs index ab47bcdc..f9bc27ab 100644 --- a/OpenAI-DotNet/Edits/Choice.cs +++ b/OpenAI-DotNet/Edits/Choice.cs @@ -4,18 +4,13 @@ namespace OpenAI.Edits { public sealed class Choice { - [JsonConstructor] - public Choice(string text, int index) - { - Text = text; - Index = index; - } - + [JsonInclude] [JsonPropertyName("text")] - public string Text { get; } + public string Text { get; private set; } + [JsonInclude] [JsonPropertyName("index")] - public int Index { get; } + public int Index { get; private set; } /// /// Gets the main text of this completion diff --git a/OpenAI-DotNet/Edits/EditRequest.cs b/OpenAI-DotNet/Edits/EditRequest.cs index a9a8c3e2..dc736aa5 100644 --- a/OpenAI-DotNet/Edits/EditRequest.cs +++ b/OpenAI-DotNet/Edits/EditRequest.cs @@ -36,7 +36,7 @@ public sealed class EditRequest if (!Model.Contains("-edit-")) { - throw new ArgumentException(nameof(model), $"{Model} does not support editing"); + throw new ArgumentException($"{Model} is not supported", nameof(model)); } Input = input; diff --git a/OpenAI-DotNet/Edits/EditResponse.cs b/OpenAI-DotNet/Edits/EditResponse.cs index 8301a27b..3113b49f 100644 --- a/OpenAI-DotNet/Edits/EditResponse.cs +++ b/OpenAI-DotNet/Edits/EditResponse.cs @@ -6,37 +6,28 @@ namespace OpenAI.Edits { public sealed class EditResponse : BaseResponse { - [JsonConstructor] - public EditResponse( - string @object, - int createdUnixTime, - IReadOnlyList choices, - Usage usage) - { - Object = @object; - CreatedUnixTime = createdUnixTime; - Choices = choices; - Usage = usage; - } - + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } /// /// The time when the result was generated in unix epoch format /// + [JsonInclude] [JsonPropertyName("created")] - public int CreatedUnixTime { get; } + public int CreatedUnixTime { get; private set; } /// The time when the result was generated [JsonIgnore] public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + [JsonInclude] [JsonPropertyName("choices")] - public IReadOnlyList Choices { get; } + public IReadOnlyList Choices { get; private set; } + [JsonInclude] [JsonPropertyName("usage")] - public Usage Usage { get; } + public Usage Usage { get; private set; } /// /// Gets the text of the first edit, representing the main result diff --git a/OpenAI-DotNet/Edits/EditsEndpoint.cs b/OpenAI-DotNet/Edits/EditsEndpoint.cs index 662f1d09..5dff8939 100644 --- a/OpenAI-DotNet/Edits/EditsEndpoint.cs +++ b/OpenAI-DotNet/Edits/EditsEndpoint.cs @@ -1,5 +1,4 @@ using OpenAI.Models; -using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -59,16 +58,8 @@ public async Task CreateEditAsync(EditRequest request) { var jsonContent = JsonSerializer.Serialize(request, Api.JsonSerializationOptions); var response = await Api.Client.PostAsync(GetEndpoint(), jsonContent.ToJsonStringContent()).ConfigureAwait(false); - var resultAsString = await response.ReadAsStringAsync().ConfigureAwait(false); - var editResponse = JsonSerializer.Deserialize(resultAsString, Api.JsonSerializationOptions); - editResponse.SetResponseData(response.Headers); - - if (editResponse == null) - { - throw new HttpRequestException($"{nameof(CreateEditAsync)} returned no results! HTTP status code: {response.StatusCode}. Response body: {resultAsString}"); - } - - return editResponse; + var responseAsString = await response.ReadAsStringAsync().ConfigureAwait(false); + return response.DeserializeResponse(responseAsString, Api.JsonSerializationOptions); } } } diff --git a/OpenAI-DotNet/Embeddings/Datum.cs b/OpenAI-DotNet/Embeddings/Datum.cs index d1a808ba..653ae252 100644 --- a/OpenAI-DotNet/Embeddings/Datum.cs +++ b/OpenAI-DotNet/Embeddings/Datum.cs @@ -5,21 +5,16 @@ namespace OpenAI.Embeddings { public sealed class Datum { - [JsonConstructor] - public Datum(string @object, IReadOnlyList embedding, int index) - { - Object = @object; - Embedding = embedding; - Index = index; - } - + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("embedding")] - public IReadOnlyList Embedding { get; } + public IReadOnlyList Embedding { get; private set; } + [JsonInclude] [JsonPropertyName("index")] - public int Index { get; } + public int Index { get; private set; } } } diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs index ebdac14a..80ffb346 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs @@ -1,6 +1,5 @@ using OpenAI.Models; using System.Collections.Generic; -using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -65,16 +64,8 @@ public async Task CreateEmbeddingAsync(EmbeddingsRequest req { var jsonContent = JsonSerializer.Serialize(request, Api.JsonSerializationOptions); var response = await Api.Client.PostAsync(GetEndpoint(), jsonContent.ToJsonStringContent()).ConfigureAwait(false); - var resultAsString = await response.ReadAsStringAsync().ConfigureAwait(false); - var embeddingsResponse = JsonSerializer.Deserialize(resultAsString, Api.JsonSerializationOptions); - embeddingsResponse.SetResponseData(response.Headers); - - if (embeddingsResponse == null) - { - throw new HttpRequestException($"{nameof(CreateEmbeddingAsync)} returned no results! HTTP status code: {response.StatusCode}. Response body: {resultAsString}"); - } - - return embeddingsResponse; + var responseAsString = await response.ReadAsStringAsync().ConfigureAwait(false); + return response.DeserializeResponse(responseAsString, Api.JsonSerializationOptions); } } } diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs index 04b6cc22..2d7bb3f7 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs @@ -58,11 +58,13 @@ public EmbeddingsRequest(IEnumerable input, Model model = null, string u throw new ArgumentNullException(nameof(input), $"Missing required {nameof(input)} parameter"); } - Model = model ?? new Model("text-embedding-ada-002"); + Model = model ?? Models.Model.Embedding_Ada_002; - if (!Model.Contains("text-embedding")) + if (!Model.Contains("embedding") && + !Model.Contains("search") && + !Model.Contains("similarity")) { - throw new ArgumentException(nameof(model), $"{Model} is not supported for embedding."); + throw new ArgumentException($"{Model} is not supported", nameof(model)); } User = user; diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsResponse.cs b/OpenAI-DotNet/Embeddings/EmbeddingsResponse.cs index 73bd831a..00c392c9 100644 --- a/OpenAI-DotNet/Embeddings/EmbeddingsResponse.cs +++ b/OpenAI-DotNet/Embeddings/EmbeddingsResponse.cs @@ -5,25 +5,20 @@ namespace OpenAI.Embeddings { public sealed class EmbeddingsResponse : BaseResponse { - [JsonConstructor] - public EmbeddingsResponse(string @object, IReadOnlyList data, string model, Usage usage) - { - Object = @object; - Data = data; - Model = model; - Usage = usage; - } - + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("data")] - public IReadOnlyList Data { get; } + public IReadOnlyList Data { get; private set; } + [JsonInclude] [JsonPropertyName("model")] - public string Model { get; } + public string Model { get; private set; } + [JsonInclude] [JsonPropertyName("usage")] - public Usage Usage { get; } + public Usage Usage { get; private set; } } } diff --git a/OpenAI-DotNet/Event.cs b/OpenAI-DotNet/Event.cs index 8454e36a..865375d5 100644 --- a/OpenAI-DotNet/Event.cs +++ b/OpenAI-DotNet/Event.cs @@ -5,33 +5,23 @@ namespace OpenAI { public sealed class Event { - [JsonConstructor] - public Event( - string @object, - int createdAtUnixTime, - string level, - string message - ) - { - Object = @object; - CreatedAtUnixTime = createdAtUnixTime; - Level = level; - Message = message; - } - + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("created_at")] - public int CreatedAtUnixTime { get; } + public int CreatedAtUnixTime { get; private set; } [JsonIgnore] public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTime).DateTime; + [JsonInclude] [JsonPropertyName("level")] - public string Level { get; } + public string Level { get; private set; } + [JsonInclude] [JsonPropertyName("message")] - public string Message { get; } + public string Message { get; private set; } } } diff --git a/OpenAI-DotNet/Files/FileData.cs b/OpenAI-DotNet/Files/FileData.cs index 7b7b03b7..2cc9cb31 100644 --- a/OpenAI-DotNet/Files/FileData.cs +++ b/OpenAI-DotNet/Files/FileData.cs @@ -5,41 +5,36 @@ namespace OpenAI.Files { public sealed class FileData { - [JsonConstructor] - public FileData(string id, string @object, int size, int createdUnixTime, string fileName, string purpose, string status) - { - Id = id; - Object = @object; - Size = size; - CreatedUnixTime = createdUnixTime; - FileName = fileName; - Purpose = purpose; - Status = status; - } - + [JsonInclude] [JsonPropertyName("id")] - public string Id { get; } + public string Id { get; private set; } + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("bytes")] - public int Size { get; } + public int Size { get; private set; } + [JsonInclude] [JsonPropertyName("created_at")] - public int CreatedUnixTime { get; } + public int CreatedUnixTime { get; private set; } [JsonIgnore] public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + [JsonInclude] [JsonPropertyName("filename")] - public string FileName { get; } + public string FileName { get; private set; } + [JsonInclude] [JsonPropertyName("purpose")] - public string Purpose { get; } + public string Purpose { get; private set; } + [JsonInclude] [JsonPropertyName("status")] - public string Status { get; } + public string Status { get; private set; } public static implicit operator string(FileData fileData) => fileData.Id; } diff --git a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs index 8f4aeb24..0be1f08e 100644 --- a/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs +++ b/OpenAI-DotNet/FineTuning/FineTuneJobResponse.cs @@ -8,65 +8,60 @@ namespace OpenAI.FineTuning { internal sealed class FineTuneJobResponse : BaseResponse { - [JsonConstructor] - public FineTuneJobResponse(string id, string @object, string model, int createdUnixTime, IReadOnlyList events, string fineTunedModel, HyperParams hyperParams, string organizationId, IReadOnlyList resultFiles, string status, IReadOnlyList validationFiles, IReadOnlyList trainingFiles, int updatedAtUnixTime) - { - Id = id; - Object = @object; - Model = model; - CreatedUnixTime = createdUnixTime; - Events = events; - FineTunedModel = fineTunedModel; - HyperParams = hyperParams; - OrganizationId = organizationId; - ResultFiles = resultFiles; - Status = status; - ValidationFiles = validationFiles; - TrainingFiles = trainingFiles; - UpdatedAtUnixTime = updatedAtUnixTime; - } - + [JsonInclude] [JsonPropertyName("id")] - public string Id { get; } + public string Id { get; private set; } + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("model")] - public string Model { get; } + public string Model { get; private set; } + [JsonInclude] [JsonPropertyName("created_at")] - public int CreatedUnixTime { get; } + public int CreatedUnixTime { get; private set; } [JsonIgnore] public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + [JsonInclude] [JsonPropertyName("events")] - public IReadOnlyList Events { get; } + public IReadOnlyList Events { get; private set; } + [JsonInclude] [JsonPropertyName("fine_tuned_model")] - public string FineTunedModel { get; } + public string FineTunedModel { get; private set; } + [JsonInclude] [JsonPropertyName("hyperparams")] - public HyperParams HyperParams { get; } + public HyperParams HyperParams { get; private set; } + [JsonInclude] [JsonPropertyName("organization_id")] - public string OrganizationId { get; } + public string OrganizationId { get; private set; } + [JsonInclude] [JsonPropertyName("result_files")] - public IReadOnlyList ResultFiles { get; } + public IReadOnlyList ResultFiles { get; private set; } + [JsonInclude] [JsonPropertyName("status")] - public string Status { get; } + public string Status { get; private set; } + [JsonInclude] [JsonPropertyName("validation_files")] - public IReadOnlyList ValidationFiles { get; } + public IReadOnlyList ValidationFiles { get; private set; } + [JsonInclude] [JsonPropertyName("training_files")] - public IReadOnlyList TrainingFiles { get; } + public IReadOnlyList TrainingFiles { get; private set; } + [JsonInclude] [JsonPropertyName("updated_at")] - public int UpdatedAtUnixTime { get; } + public int UpdatedAtUnixTime { get; private set; } [JsonIgnore] public DateTime UpdatedAt => DateTimeOffset.FromUnixTimeSeconds(UpdatedAtUnixTime).DateTime; diff --git a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs index 654a3257..acbf6cff 100644 --- a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs +++ b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs @@ -52,9 +52,7 @@ public async Task CreateFineTuneJobAsync(CreateFineTuneJobRequest j var jsonContent = JsonSerializer.Serialize(jobRequest, Api.JsonSerializationOptions); var response = await Api.Client.PostAsync(GetEndpoint(), jsonContent.ToJsonStringContent()).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync().ConfigureAwait(false); - var result = JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); - result.SetResponseData(response.Headers); - return result; + return response.DeserializeResponse(responseAsString, Api.JsonSerializationOptions); } /// @@ -79,9 +77,7 @@ public async Task RetrieveFineTuneJobInfoAsync(string jobId) { var response = await Api.Client.GetAsync($"{GetEndpoint()}/{jobId}").ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync().ConfigureAwait(false); - var result = JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); - result.SetResponseData(response.Headers); - return result; + return response.DeserializeResponse(responseAsString, Api.JsonSerializationOptions); } /// @@ -94,8 +90,7 @@ public async Task CancelFineTuneJobAsync(string jobId) { var response = await Api.Client.PostAsync($"{GetEndpoint()}/{jobId}/cancel", null!).ConfigureAwait(false); var responseAsString = await response.ReadAsStringAsync().ConfigureAwait(false); - var result = JsonSerializer.Deserialize(responseAsString, Api.JsonSerializationOptions); - result.SetResponseData(response.Headers); + var result = response.DeserializeResponse(responseAsString, Api.JsonSerializationOptions); return result.Status == "cancelled"; } diff --git a/OpenAI-DotNet/FineTuning/HyperParams.cs b/OpenAI-DotNet/FineTuning/HyperParams.cs index d15fb064..2e5d80a9 100644 --- a/OpenAI-DotNet/FineTuning/HyperParams.cs +++ b/OpenAI-DotNet/FineTuning/HyperParams.cs @@ -13,16 +13,20 @@ public HyperParams(int? batchSize, double? learningRateMultiplier, int epochs, d PromptLossWeight = promptLossWeight; } + [JsonInclude] [JsonPropertyName("batch_size")] - public int? BatchSize { get; } + public int? BatchSize { get; private set; } + [JsonInclude] [JsonPropertyName("learning_rate_multiplier")] - public double? LearningRateMultiplier { get; } + public double? LearningRateMultiplier { get; private set; } + [JsonInclude] [JsonPropertyName("n_epochs")] - public int Epochs { get; } + public int Epochs { get; private set; } + [JsonInclude] [JsonPropertyName("prompt_loss_weight")] - public double PromptLossWeight { get; } + public double PromptLossWeight { get; private set; } } } diff --git a/OpenAI-DotNet/Images/ImageResult.cs b/OpenAI-DotNet/Images/ImageResult.cs index d99a3b80..e8e8157a 100644 --- a/OpenAI-DotNet/Images/ImageResult.cs +++ b/OpenAI-DotNet/Images/ImageResult.cs @@ -4,13 +4,8 @@ namespace OpenAI.Images { internal class ImageResult { - [JsonConstructor] - public ImageResult(string url) - { - Url = url; - } - + [JsonInclude] [JsonPropertyName("url")] - public string Url { get; } + public string Url { get; private set; } } } diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs index 715837ba..ae28022a 100644 --- a/OpenAI-DotNet/Images/ImagesEndpoint.cs +++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; diff --git a/OpenAI-DotNet/Images/ImagesResponse.cs b/OpenAI-DotNet/Images/ImagesResponse.cs index b50d3203..b2ca4548 100644 --- a/OpenAI-DotNet/Images/ImagesResponse.cs +++ b/OpenAI-DotNet/Images/ImagesResponse.cs @@ -5,17 +5,12 @@ namespace OpenAI.Images { internal class ImagesResponse : BaseResponse { - [JsonConstructor] - public ImagesResponse(int created, IReadOnlyList data) - { - Created = created; - Data = data; - } - + [JsonInclude] [JsonPropertyName("created")] - public int Created { get; } + public int Created { get; private set; } + [JsonInclude] [JsonPropertyName("data")] - public IReadOnlyList Data { get; } + public IReadOnlyList Data { get; private set; } } } diff --git a/OpenAI-DotNet/Models/Model.cs b/OpenAI-DotNet/Models/Model.cs index c50a59f8..da3c732b 100644 --- a/OpenAI-DotNet/Models/Model.cs +++ b/OpenAI-DotNet/Models/Model.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace OpenAI.Models { /// - /// Represents a language model. + /// Represents a language model.
+ /// ///
public sealed class Model { @@ -17,21 +19,6 @@ public Model(string id) Id = id; } - [JsonConstructor] - public Model( - string id, - string @object, - string ownedBy, - IReadOnlyList permissions, - string root, string parent) : this(id) - { - Object = @object; - OwnedBy = ownedBy; - Permissions = permissions; - Root = root; - Parent = parent; - } - /// /// Allows a model to be implicitly cast to the string of its id. /// @@ -46,27 +33,34 @@ public Model(string id) /// public override string ToString() => Id; + [JsonInclude] [JsonPropertyName("id")] - public string Id { get; } + public string Id { get; private set; } + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("owned_by")] public string OwnedBy { get; private set; } + [JsonInclude] [JsonPropertyName("permissions")] - public IReadOnlyList Permissions { get; } + public IReadOnlyList Permissions { get; private set; } + [JsonInclude] [JsonPropertyName("root")] - public string Root { get; } + public string Root { get; private set; } + [JsonInclude] [JsonPropertyName("parent")] - public string Parent { get; } + public string Parent { get; private set; } /// /// The default Model to use in the case no other is specified. Defaults to /// + [Obsolete("Will be removed in next major release.")] public static Model Default => Davinci; /// @@ -98,5 +92,20 @@ public Model(string id) /// Good at: Parsing text, simple classification, address correction, keywords /// public static Model Ada { get; } = new Model("text-ada-001") { OwnedBy = "openai" }; + + /// + /// The default model for . + /// + public static Model Embedding_Ada_002 { get; } = new Model("text-embedding-ada-002") { OwnedBy = "openai" }; + + /// + /// The default model for . + /// + public static Model Whisper1 { get; } = new Model("whisper-1") { OwnedBy = "openai" }; + + /// + /// The default model for . + /// + public static Model Moderation_Latest { get; } = new Model("text-moderation-latest") { OwnedBy = "openai" }; } } diff --git a/OpenAI-DotNet/Models/Permission.cs b/OpenAI-DotNet/Models/Permission.cs index af7d246d..c252a02f 100644 --- a/OpenAI-DotNet/Models/Permission.cs +++ b/OpenAI-DotNet/Models/Permission.cs @@ -5,60 +5,55 @@ namespace OpenAI.Models { public sealed class Permission { - [JsonConstructor] - public Permission(string id, string @object, int createdAtUnixTime, bool allowCreateEngine, bool allowSampling, bool allowLogprobs, bool allowSearchIndices, bool allowView, bool allowFineTuning, string organization, object group, bool isBlocking) - { - Id = id; - Object = @object; - CreatedAtUnixTime = createdAtUnixTime; - AllowCreateEngine = allowCreateEngine; - AllowSampling = allowSampling; - AllowLogprobs = allowLogprobs; - AllowSearchIndices = allowSearchIndices; - AllowView = allowView; - AllowFineTuning = allowFineTuning; - Organization = organization; - Group = group; - IsBlocking = isBlocking; - } - + [JsonInclude] [JsonPropertyName("id")] - public string Id { get; } + public string Id { get; private set; } + [JsonInclude] [JsonPropertyName("object")] - public string Object { get; } + public string Object { get; private set; } + [JsonInclude] [JsonPropertyName("created")] - public int CreatedAtUnixTime { get; } + public int CreatedAtUnixTime { get; private set; } [JsonIgnore] public DateTime CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnixTime).DateTime; + [JsonInclude] [JsonPropertyName("allow_create_engine")] - public bool AllowCreateEngine { get; } + public bool AllowCreateEngine { get; private set; } + [JsonInclude] [JsonPropertyName("allow_sampling")] - public bool AllowSampling { get; } + public bool AllowSampling { get; private set; } + [JsonInclude] [JsonPropertyName("allow_logprobs")] - public bool AllowLogprobs { get; } + public bool AllowLogprobs { get; private set; } + [JsonInclude] [JsonPropertyName("allow_search_indices")] - public bool AllowSearchIndices { get; } + public bool AllowSearchIndices { get; private set; } + [JsonInclude] [JsonPropertyName("allow_view")] - public bool AllowView { get; } + public bool AllowView { get; private set; } + [JsonInclude] [JsonPropertyName("allow_fine_tuning")] - public bool AllowFineTuning { get; } + public bool AllowFineTuning { get; private set; } + [JsonInclude] [JsonPropertyName("organization")] - public string Organization { get; } + public string Organization { get; private set; } + [JsonInclude] [JsonPropertyName("group")] - public object Group { get; } + public object Group { get; private set; } + [JsonInclude] [JsonPropertyName("is_blocking")] - public bool IsBlocking { get; } + public bool IsBlocking { get; private set; } } } diff --git a/OpenAI-DotNet/Moderations/Categories.cs b/OpenAI-DotNet/Moderations/Categories.cs index 04a6fdc2..7d29486b 100644 --- a/OpenAI-DotNet/Moderations/Categories.cs +++ b/OpenAI-DotNet/Moderations/Categories.cs @@ -4,37 +4,32 @@ namespace OpenAI.Moderations { public sealed class Categories { - [JsonConstructor] - public Categories(bool hate, bool hateThreatening, bool selfHarm, bool sexual, bool sexualMinors, bool violence, bool violenceGraphic) - { - Hate = hate; - HateThreatening = hateThreatening; - SelfHarm = selfHarm; - Sexual = sexual; - SexualMinors = sexualMinors; - Violence = violence; - ViolenceGraphic = violenceGraphic; - } - + [JsonInclude] [JsonPropertyName("hate")] - public bool Hate { get; } + public bool Hate { get; private set; } + [JsonInclude] [JsonPropertyName("hate/threatening")] - public bool HateThreatening { get; } + public bool HateThreatening { get; private set; } + [JsonInclude] [JsonPropertyName("self-harm")] - public bool SelfHarm { get; } + public bool SelfHarm { get; private set; } + [JsonInclude] [JsonPropertyName("sexual")] - public bool Sexual { get; } + public bool Sexual { get; private set; } + [JsonInclude] [JsonPropertyName("sexual/minors")] - public bool SexualMinors { get; } + public bool SexualMinors { get; private set; } + [JsonInclude] [JsonPropertyName("violence")] - public bool Violence { get; } + public bool Violence { get; private set; } + [JsonInclude] [JsonPropertyName("violence/graphic")] - public bool ViolenceGraphic { get; } + public bool ViolenceGraphic { get; private set; } } } diff --git a/OpenAI-DotNet/Moderations/ModerationResult.cs b/OpenAI-DotNet/Moderations/ModerationResult.cs index f9aea10c..83ece0e3 100644 --- a/OpenAI-DotNet/Moderations/ModerationResult.cs +++ b/OpenAI-DotNet/Moderations/ModerationResult.cs @@ -4,21 +4,16 @@ namespace OpenAI.Moderations { public sealed class ModerationResult { - [JsonConstructor] - public ModerationResult(Categories categories, Scores scores, bool flagged) - { - Categories = categories; - Scores = scores; - Flagged = flagged; - } - + [JsonInclude] [JsonPropertyName("categories")] - public Categories Categories { get; } + public Categories Categories { get; private set; } + [JsonInclude] [JsonPropertyName("category_scores")] - public Scores Scores { get; } + public Scores Scores { get; private set; } + [JsonInclude] [JsonPropertyName("flagged")] - public bool Flagged { get; } + public bool Flagged { get; private set; } } } diff --git a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs index 5b8f8c97..4288b404 100644 --- a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs +++ b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs @@ -54,18 +54,10 @@ public async Task GetModerationAsync(string input, Model model = null) /// Raised when the HTTP request fails public async Task CreateModerationAsync(ModerationsRequest request) { - var jsonContent = JsonSerializer.Serialize(request, Api.JsonSerializationOptions); - var response = await Api.Client.PostAsync(GetEndpoint(), jsonContent.ToJsonStringContent()).ConfigureAwait(false); + var jsonContent = JsonSerializer.Serialize(request, Api.JsonSerializationOptions).ToJsonStringContent(); + var response = await Api.Client.PostAsync(GetEndpoint(), jsonContent).ConfigureAwait(false); var resultAsString = await response.ReadAsStringAsync().ConfigureAwait(false); - var moderationResponse = JsonSerializer.Deserialize(resultAsString, Api.JsonSerializationOptions); - - if (moderationResponse == null) - { - throw new HttpRequestException($"{nameof(CreateModerationAsync)} returned no results! HTTP status code: {response.StatusCode}. Response body: {resultAsString}"); - } - - moderationResponse.SetResponseData(response.Headers); - return moderationResponse; + return response.DeserializeResponse(resultAsString, Api.JsonSerializationOptions); } } } diff --git a/OpenAI-DotNet/Moderations/ModerationsRequest.cs b/OpenAI-DotNet/Moderations/ModerationsRequest.cs index 3cfdee3c..7d5dd169 100644 --- a/OpenAI-DotNet/Moderations/ModerationsRequest.cs +++ b/OpenAI-DotNet/Moderations/ModerationsRequest.cs @@ -24,11 +24,11 @@ public sealed class ModerationsRequest public ModerationsRequest(string input, Model model = null) { Input = input; - Model = model ?? new Model("text-moderation-latest"); + Model = model ?? Models.Model.Moderation_Latest; if (!Model.Contains("text-moderation")) { - throw new ArgumentException(nameof(model), $"{Model} is not supported."); + throw new ArgumentException($"{Model} is not supported", nameof(model)); } } diff --git a/OpenAI-DotNet/Moderations/ModerationsResponse.cs b/OpenAI-DotNet/Moderations/ModerationsResponse.cs index 25616a69..9c5b9de7 100644 --- a/OpenAI-DotNet/Moderations/ModerationsResponse.cs +++ b/OpenAI-DotNet/Moderations/ModerationsResponse.cs @@ -5,21 +5,16 @@ namespace OpenAI.Moderations { public sealed class ModerationsResponse : BaseResponse { - [JsonConstructor] - public ModerationsResponse(string id, string model, IReadOnlyList results) - { - Id = id; - Model = model; - Results = results; - } - + [JsonInclude] [JsonPropertyName("id")] - public string Id { get; } + public string Id { get; private set; } + [JsonInclude] [JsonPropertyName("model")] - public string Model { get; } + public string Model { get; private set; } + [JsonInclude] [JsonPropertyName("results")] - public IReadOnlyList Results { get; } + public IReadOnlyList Results { get; private set; } } } diff --git a/OpenAI-DotNet/Moderations/Scores.cs b/OpenAI-DotNet/Moderations/Scores.cs index 5f810c8f..3d0f6dc6 100644 --- a/OpenAI-DotNet/Moderations/Scores.cs +++ b/OpenAI-DotNet/Moderations/Scores.cs @@ -4,37 +4,32 @@ namespace OpenAI.Moderations { public sealed class Scores { - [JsonConstructor] - public Scores(double hate, double hateThreatening, double selfHarm, double sexual, double sexualMinors, double violence, double violenceGraphic) - { - Hate = hate; - HateThreatening = hateThreatening; - SelfHarm = selfHarm; - Sexual = sexual; - SexualMinors = sexualMinors; - Violence = violence; - ViolenceGraphic = violenceGraphic; - } - + [JsonInclude] [JsonPropertyName("hate")] - public double Hate { get; } + public double Hate { get; private set; } + [JsonInclude] [JsonPropertyName("hate/threatening")] - public double HateThreatening { get; } + public double HateThreatening { get; private set; } + [JsonInclude] [JsonPropertyName("self-harm")] - public double SelfHarm { get; } + public double SelfHarm { get; private set; } + [JsonInclude] [JsonPropertyName("sexual")] - public double Sexual { get; } + public double Sexual { get; private set; } + [JsonInclude] [JsonPropertyName("sexual/minors")] - public double SexualMinors { get; } + public double SexualMinors { get; private set; } + [JsonInclude] [JsonPropertyName("violence")] - public double Violence { get; } + public double Violence { get; private set; } + [JsonInclude] [JsonPropertyName("violence/graphic")] - public double ViolenceGraphic { get; } + public double ViolenceGraphic { get; private set; } } } diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 3bf5c69b..d40efbb0 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -15,7 +15,13 @@ Based on OpenAI-API by OKGoDoIt (Roger Pincombe) https://github.com/RageAgainstThePixel/OpenAI-DotNet OpenAI, AI, ML, API, gpt, gpt-3, chatGPT OpenAI API - Bump version to 5.1.0 + Bump version to 5.1.1 +- Refactored Model validation +- Added additional default models +- Deprecate OpenAIClient.DefaultModel +- Implemented chat completion streaming +- Refactored immutable types +Bump version to 5.1.0 - Add support for Audio endpoint and Whisper models - Audio Speech to text - Audio translation @@ -28,7 +34,7 @@ Bump version to 5.0.0 - Added Chat endpoint Bump version to 4.4.4 - ImageEditRequest mask is now optional so long as texture has alpha transparency -- ImageVariationRequest added constructor overload for memory stream image +- ImageVariationRequest added constructor overload for memory stream image - Updated AuthInfo parameter validation - Renamed OPEN_AI_ORGANIZATION_ID -> OPENAI_ORGANIZATION_ID Bump version to 4.4.3 @@ -53,9 +59,9 @@ Bump version to 4.4.0 OpenAI-DotNet.pfx true OpenAI-DotNet - 5.1.0 - 5.1.0.0 - 5.1.0.0 + 5.1.1 + 5.1.1.0 + 5.1.1.0 RageAgainstThePixel README.md diff --git a/OpenAI-DotNet/OpenAIClient.cs b/OpenAI-DotNet/OpenAIClient.cs index e6fb5ead..109f8e01 100644 --- a/OpenAI-DotNet/OpenAIClient.cs +++ b/OpenAI-DotNet/OpenAIClient.cs @@ -8,6 +8,7 @@ using OpenAI.Images; using OpenAI.Models; using OpenAI.Moderations; +using System; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Authentication; @@ -27,18 +28,27 @@ public sealed class OpenAIClient /// The /model to use for API calls, /// defaulting to if not specified. /// Raised when authentication details are missing or invalid. + [Obsolete("Will be removed in next major release.")] public OpenAIClient(Model model) : this(null, model) { } + /// + /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints + /// + /// + /// The /model to use for API calls, + /// defaulting to if not specified. + /// Raised when authentication details are missing or invalid. + [Obsolete("Will be removed in next major release.")] + public OpenAIClient(OpenAIAuthentication openAIAuthentication, Model model) : this(openAIAuthentication) { } + /// /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints /// /// The API authentication information to use for API calls, /// or to attempt to use the , /// potentially loading from environment vars or from a config file. - /// The to use for API calls, - /// defaulting to if not specified. /// Raised when authentication details are missing or invalid. - public OpenAIClient(OpenAIAuthentication openAIAuthentication = null, Model model = null) + public OpenAIClient(OpenAIAuthentication openAIAuthentication = null) { OpenAIAuthentication = openAIAuthentication ?? OpenAIAuthentication.Default; @@ -61,7 +71,6 @@ public OpenAIClient(OpenAIAuthentication openAIAuthentication = null, Model mode { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - DefaultModel = model ?? Model.Default; ModelsEndpoint = new ModelsEndpoint(this); CompletionsEndpoint = new CompletionsEndpoint(this); ChatEndpoint = new ChatEndpoint(this); @@ -92,6 +101,7 @@ public OpenAIClient(OpenAIAuthentication openAIAuthentication = null, Model mode /// /// Specifies which to use for API calls /// + [Obsolete("Will be removed in next major release.")] public Model DefaultModel { get; set; } private int version; diff --git a/OpenAI-DotNet/ResponseExtensions.cs b/OpenAI-DotNet/ResponseExtensions.cs index bef48c34..417c08cd 100644 --- a/OpenAI-DotNet/ResponseExtensions.cs +++ b/OpenAI-DotNet/ResponseExtensions.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -41,5 +42,12 @@ internal static async Task CheckResponseAsync(this HttpResponseMessage response, throw new HttpRequestException($"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}"); } } + + internal static T DeserializeResponse(this HttpResponseMessage response, string json, JsonSerializerOptions settings) where T : BaseResponse + { + var result = JsonSerializer.Deserialize(json, settings); + result.SetResponseData(response.Headers); + return result; + } } } diff --git a/OpenAI-DotNet/Usage.cs b/OpenAI-DotNet/Usage.cs index bde30c22..d146bad1 100644 --- a/OpenAI-DotNet/Usage.cs +++ b/OpenAI-DotNet/Usage.cs @@ -4,24 +4,16 @@ namespace OpenAI { public sealed class Usage { - [JsonConstructor] - public Usage( - int promptTokens, - int completionTokens, - int totalTokens) - { - PromptTokens = promptTokens; - CompletionTokens = completionTokens; - TotalTokens = totalTokens; - } - + [JsonInclude] [JsonPropertyName("prompt_tokens")] - public int PromptTokens { get; } + public int PromptTokens { get; private set; } + [JsonInclude] [JsonPropertyName("completion_tokens")] - public int CompletionTokens { get; } + public int CompletionTokens { get; private set; } + [JsonInclude] [JsonPropertyName("total_tokens")] - public int TotalTokens { get; } + public int TotalTokens { get; private set; } } } diff --git a/README.md b/README.md index 4e2fe09e..05489a1e 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,39 @@ Console.WriteLine(result.FirstChoice); ##### [Chat Streaming](https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream) ```csharp -TODO +var api = new OpenAIClient(); +var chatPrompts = new List +{ + new ChatPrompt("system", "You are a helpful assistant."), + new ChatPrompt("user", "Who won the world series in 2020?"), + new ChatPrompt("assistant", "The Los Angeles Dodgers won the World Series in 2020."), + new ChatPrompt("user", "Where was it played?"), +}; +var chatRequest = new ChatRequest(chatPrompts, Model.GPT3_5_Turbo); + +await api.ChatEndpoint.StreamCompletionAsync(chatRequest, result => +{ + Console.WriteLine(result.FirstChoice); +}); +``` + +Or if using [`IAsyncEnumerable{T}`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-5.0) ([C# 8.0+](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8)) + +```csharp +var api = new OpenAIClient(); +var chatPrompts = new List +{ + new ChatPrompt("system", "You are a helpful assistant."), + new ChatPrompt("user", "Who won the world series in 2020?"), + new ChatPrompt("assistant", "The Los Angeles Dodgers won the World Series in 2020."), + new ChatPrompt("user", "Where was it played?"), +}; +var chatRequest = new ChatRequest(chatPrompts, Model.GPT3_5_Turbo); + +await foreach (var result in api.ChatEndpoint.StreamCompletionEnumerableAsync(chatRequest)) +{ + Console.WriteLine(result.FirstChoice); +} ``` ### [Edits](https://beta.openai.com/docs/api-reference/edits)