From c98ddcaae3be88821486743e405960f5b31290b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Milano?= Date: Fri, 9 Sep 2022 11:45:35 -0300 Subject: [PATCH 1/9] - Add FilesEndpoint (endpoint needed for FineTunning) --- OpenAI_API.sln | 4 +- OpenAI_API/BaseEndpoint.cs | 41 +++++++++ OpenAI_API/Files/File.cs | 69 +++++++++++++++ OpenAI_API/Files/FilesEndpoint.cs | 128 ++++++++++++++++++++++++++++ OpenAI_API/OpenAIAPI.cs | 12 ++- OpenAI_Tests/FilesEndpointTests.cs | 91 ++++++++++++++++++++ OpenAI_Tests/OpenAI_Tests.csproj | 6 ++ OpenAI_Tests/fine-tuning-data.jsonl | 75 ++++++++++++++++ 8 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 OpenAI_API/BaseEndpoint.cs create mode 100644 OpenAI_API/Files/File.cs create mode 100644 OpenAI_API/Files/FilesEndpoint.cs create mode 100644 OpenAI_Tests/FilesEndpointTests.cs create mode 100644 OpenAI_Tests/fine-tuning-data.jsonl diff --git a/OpenAI_API.sln b/OpenAI_API.sln index 1f1864f..9412cfe 100644 --- a/OpenAI_API.sln +++ b/OpenAI_API.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30309.148 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI_API", "OpenAI_API\OpenAI_API.csproj", "{99C80D3E-3F0F-4ACC-900D-7AAE6230A780}" EndProject diff --git a/OpenAI_API/BaseEndpoint.cs b/OpenAI_API/BaseEndpoint.cs new file mode 100644 index 0000000..d4a21a8 --- /dev/null +++ b/OpenAI_API/BaseEndpoint.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API +{ + public abstract class BaseEndpoint + { + protected readonly OpenAIAPI api; + + public BaseEndpoint(OpenAIAPI api) + { + this.api = api; + } + + protected abstract string GetEndpoint(); + + protected string GetUrl() + { + return $"{OpenAIAPI.API_URL}{GetEndpoint()}"; + } + + protected HttpClient GetClient() + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", api.Auth?.ThisOrDefault().ApiKey); + client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); + return client; + } + + protected string GetErrorMessage(string resultAsString, HttpResponseMessage response, string name, string description = "") + { + return $"Error at {name} ( {description} ) with HTTP status code: {response.StatusCode} . Content: {resultAsString}"; + } + + + } +} diff --git a/OpenAI_API/Files/File.cs b/OpenAI_API/Files/File.cs new file mode 100644 index 0000000..e6054e7 --- /dev/null +++ b/OpenAI_API/Files/File.cs @@ -0,0 +1,69 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API.Files +{ + public class File + { + /// + /// unique id for this file, so that it can be referenced in other operations + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// the name of the file + /// + [JsonProperty("filename")] + public string Name { get; set; } + + + /// + /// What is the purpose of this file, fine-tune, search, etc + /// + [JsonProperty("Purpose")] + public string Purpose { get; set; } + + /// + /// object type, ie: file, image, etc + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// + /// The size of the file in bytes + /// + [JsonProperty("bytes")] + public long Bytes { get; set; } + + /// + /// Timestamp for the creation time of this file + /// + [JsonProperty("created_at")] + public long CreatedAt { get; set; } + + /// + /// The object was deleted, this attribute is used in the Delete file operation + /// + [JsonProperty("deleted")] + public bool Deleted { get; set; } + + + /// + /// The status of the File (ie when an upload operation was done: "uploaded") + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// The status details, it could be null + /// + [JsonProperty("status_details")] + public string StatusDetails { get; set; } + + } +} diff --git a/OpenAI_API/Files/FilesEndpoint.cs b/OpenAI_API/Files/FilesEndpoint.cs new file mode 100644 index 0000000..5a93a13 --- /dev/null +++ b/OpenAI_API/Files/FilesEndpoint.cs @@ -0,0 +1,128 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API.Files +{ + /// + /// The API endpoint for operations List, Upload, Delete, Retrieve files + /// + public class FilesEndpoint : BaseEndpoint + { + public FilesEndpoint(OpenAIAPI api) : base(api) {} + + protected override string GetEndpoint() { return "files"; } + + /// + /// Get the list of all files + /// + /// + /// + public async Task> GetFilesAsync() + { + var response = await GetClient().GetAsync(GetUrl()); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var files = JsonConvert.DeserializeObject(resultAsString).Data; + return files; + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "List files", "Get the list of all files")); + } + + /// + /// Returns information about a specific file + /// + /// The ID of the file to use for this request + /// + public async Task GetFileAsync(string fileId) + { + var response = await GetClient().GetAsync($"{GetUrl()}/{fileId}"); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var file = JsonConvert.DeserializeObject(resultAsString); + return file; + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Retrieve file")); + } + + + /// + /// Returns the contents of the specific file as string + /// + /// The ID of the file to use for this request + /// + public async Task GetFileContentAsStringAsync(string fileId) + { + var response = await GetClient().GetAsync($"{GetUrl()}/{fileId}/content"); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + return resultAsString; + + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Retrieve file content")); + } + + /// + /// Delete a file + /// + /// The ID of the file to use for this request + /// + public async Task DeleteFileAsync(string fileId) + { + var response = await GetClient().DeleteAsync($"{GetUrl()}/{fileId}"); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var file = JsonConvert.DeserializeObject(resultAsString); + return file; + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Delete file")); + } + + /// + /// Upload a file that contains document(s) to be used across various endpoints/features. Currently, the size of all the files uploaded by one organization can be up to 1 GB. Please contact us if you need to increase the storage limit + /// + /// The name of the file to use for this request + /// The intendend purpose of the uploaded documents. Use "fine-tune" for Fine-tuning. This allows us to validate the format of the uploaded file. + public async Task UploadFileAsync(string file, string purpose = "fine-tune") + { + HttpClient client = GetClient(); + var content = new MultipartFormDataContent + { + { new StringContent(purpose), "purpose" }, + { new ByteArrayContent(System.IO.File.ReadAllBytes(file)), "file", Path.GetFileName(file) } + }; + var response = await client.PostAsync($"{GetUrl()}", content); + string resultAsString = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(resultAsString); + } + throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Upload file", "Upload a file that contains document(s) to be used across various endpoints/features.")); + } + } + /// + /// A helper class to deserialize the JSON API responses. This should not be used directly. + /// + class FilesData + { + [JsonProperty("data")] + public List Data { get; set; } + [JsonProperty("object")] + public string Obj { get; set; } + } +} diff --git a/OpenAI_API/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index 2baedea..27c419a 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using OpenAI_API.Files; using System; using System.Collections.Generic; using System.IO; @@ -14,6 +15,11 @@ namespace OpenAI_API /// public class OpenAIAPI { + /// + /// Base url for OpenAI + /// + public const string API_URL = "https://api.openai.com/v1/"; + /// /// The API authentication information to use for API calls /// @@ -36,6 +42,7 @@ public OpenAIAPI(APIAuthentication apiKeys = null, Engine engine = null) Completions = new CompletionEndpoint(this); Engines = new EnginesEndpoint(this); Search = new SearchEndpoint(this); + Files = new FilesEndpoint(this); } /// @@ -53,7 +60,10 @@ public OpenAIAPI(APIAuthentication apiKeys = null, Engine engine = null) /// public SearchEndpoint Search { get; } - + /// + /// The API lets you do operations with files. You can upload, delete or retrieve files. Files can be used for fine-tuning, search, etc. + /// + public FilesEndpoint Files { get; } diff --git a/OpenAI_Tests/FilesEndpointTests.cs b/OpenAI_Tests/FilesEndpointTests.cs new file mode 100644 index 0000000..c824a7b --- /dev/null +++ b/OpenAI_Tests/FilesEndpointTests.cs @@ -0,0 +1,91 @@ +using NUnit.Framework; +using OpenAI_API; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenAI_Tests +{ + public class FilesEndpointTests + { + [SetUp] + public void Setup() + { + OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); + } + + [Test] + [Order(1)] + public async Task UploadFile() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.UploadFileAsync("fine-tuning-data.jsonl"); + Assert.IsNotNull(response); + Assert.IsTrue(response.Id.Length > 0); + Assert.IsTrue(response.Object == "file"); + Assert.IsTrue(response.Bytes > 0); + Assert.IsTrue(response.CreatedAt > 0); + Assert.IsTrue(response.Status == "uploaded"); + // The file must be processed before it can be used in other operations, so for testing purposes we just sleep awhile. + Thread.Sleep(10000); + } + + [Test] + [Order(2)] + public async Task ListFiles() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.GetFilesAsync(); + + foreach (var file in response) + { + Assert.IsNotNull(file); + Assert.IsTrue(file.Id.Length > 0); + } + } + + + [Test] + [Order(3)] + public async Task GetFile() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.GetFilesAsync(); + foreach (var file in response) + { + Assert.IsNotNull(file); + Assert.IsTrue(file.Id.Length > 0); + string id = file.Id; + if (file.Name == "fine-tuning-data.jsonl") + { + var fileResponse = await api.Files.GetFileAsync(file.Id); + Assert.IsNotNull(fileResponse); + Assert.IsTrue(fileResponse.Id == id); + } + } + } + + [Test] + [Order(4)] + public async Task DeleteFiles() + { + var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var response = await api.Files.GetFilesAsync(); + foreach (var file in response) + { + Assert.IsNotNull(file); + Assert.IsTrue(file.Id.Length > 0); + if (file.Name == "fine-tuning-data.jsonl") + { + var deleteResponse = await api.Files.DeleteFileAsync(file.Id); + Assert.IsNotNull(deleteResponse); + Assert.IsTrue(deleteResponse.Deleted); + } + } + } + + } +} diff --git a/OpenAI_Tests/OpenAI_Tests.csproj b/OpenAI_Tests/OpenAI_Tests.csproj index 56b6dea..9da037d 100644 --- a/OpenAI_Tests/OpenAI_Tests.csproj +++ b/OpenAI_Tests/OpenAI_Tests.csproj @@ -16,4 +16,10 @@ + + + PreserveNewest + + + diff --git a/OpenAI_Tests/fine-tuning-data.jsonl b/OpenAI_Tests/fine-tuning-data.jsonl new file mode 100644 index 0000000..d903bdb --- /dev/null +++ b/OpenAI_Tests/fine-tuning-data.jsonl @@ -0,0 +1,75 @@ +{ "prompt": "type for FilterRelationType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for FilterOperation", "completion":"Numeric(4.0).###"} +{ "prompt": "type for TargetType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for RuntimeEnvironment", "completion":"Numeric(4.0).###"} +{ "prompt": "type for MapType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for LogLevel", "completion":"Numeric(4.0).###"} +{ "prompt": "type for StorePurchaseState", "completion":"Numeric(1.0).###"} +{ "prompt": "type for StorePurchasePlatform", "completion":"Numeric(4.0).###"} +{ "prompt": "type for StoreProductType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for StorePurchaseStatus", "completion":"Numeric(4.0).###"} +{ "prompt": "type for MediaMetadataKey", "completion":"VarChar(50).###"} +{ "prompt": "type for MediaStreamType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for DeviceAuthenticationPolicy", "completion":"Numeric(1.0).###"} +{ "prompt": "type for Url", "completion":"VarChar(1000).###"} +{ "prompt": "type for IMEMode", "completion":"Character(40).###"} +{ "prompt": "type for Time", "completion":"DateTime.###"} +{ "prompt": "type for Encoding", "completion":"Character(256).###"} +{ "prompt": "type for Timezones", "completion":"Character(60).###"} +{ "prompt": "type for Effect", "completion":"Character(20).###"} +{ "prompt": "type for CallType", "completion":"Character(20).###"} +{ "prompt": "type for CryptoEncryptAlgorithm", "completion":"Character(40).###"} +{ "prompt": "type for CryptoHashAlgorithm", "completion":"Character(40).###"} +{ "prompt": "type for CryptoSignAlgorithm", "completion":"Character(40).###"} +{ "prompt": "type for TrnMode", "completion":"Character(3).###"} +{ "prompt": "type for Address", "completion":"VarChar(1K).###"} +{ "prompt": "type for Component", "completion":"VarChar(1000).###"} +{ "prompt": "type for Email", "completion":"VarChar(100).###"} +{ "prompt": "type for Geolocation", "completion":"Character(50).###"} +{ "prompt": "type for Html", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for Phone", "completion":"Character(20).###"} +{ "prompt": "type for APIAuthorizationStatus", "completion":"Numeric(1.0).###"} +{ "prompt": "type for MessageTypes", "completion":"Numeric(2.0).###"} +{ "prompt": "type for ProgressIndicatorType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for RecentLinksOptions", "completion":"Numeric(4.0).###"} +{ "prompt": "type for ObjectName", "completion":"VarChar(256).###"} +{ "prompt": "type for CallTargetSize", "completion":"Character(10).###"} +{ "prompt": "type for EventExecution", "completion":"Numeric(1.0).###"} +{ "prompt": "type for PushNotificationPriority", "completion":"Character(20).###"} +{ "prompt": "type for SmartDeviceType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for CameraAPIQuality", "completion":"Numeric(1.0).###"} +{ "prompt": "type for AudioAPISessionType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for MediaDuration", "completion":"Numeric(12.0).###"} +{ "prompt": "type for PlaybackState", "completion":"Numeric(4.0).###"} +{ "prompt": "type for NetworkAPIConnectionType", "completion":"Numeric(1.0).###"} +{ "prompt": "type for EventAction", "completion":"Numeric(4.0).###"} +{ "prompt": "type for EventStatus", "completion":"Numeric(4.0).###"} +{ "prompt": "type for EventData", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for EventErrors", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for ApplicationState", "completion":"Numeric(1.0).###"} +{ "prompt": "type for SynchronizationReceiveResult", "completion":"Numeric(4.0).###"} +{ "prompt": "type for RegionState", "completion":"Numeric(1.0).###"} +{ "prompt": "type for BeaconProximity", "completion":"Numeric(1.0).###"} +{ "prompt": "type for MediaFinishReason", "completion":"Numeric(4.0).###"} +{ "prompt": "type for HttpMethod", "completion":"Character(7).###"} +{ "prompt": "type for HttpAuthenticationType", "completion":"Numeric(4.0).###"} +{ "prompt": "type for CommonCallTarget", "completion":"Character(20).###"} +{ "prompt": "type for BarcodeType", "completion":"VarChar(40).###"} +{ "prompt": "type for Name", "completion":"VarChar(100).###"} +{ "prompt": "type for ContactData", "completion":"VarChar(80).###"} +{ "prompt": "type for Lang", "completion":"Character(3).###"} +{ "prompt": "type for Bio", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for FullName", "completion":"VarChar(150).###"} +{ "prompt": "type for Status", "completion":"Character(1).###"} +{ "prompt": "type for Id", "completion":"Numeric(8.0).###"} +{ "prompt": "type for SessionType", "completion":"Character(1).###"} +{ "prompt": "type for Title", "completion":"VarChar(160).###"} +{ "prompt": "type for Abstract", "completion":"VarChar(1000).###"} +{ "prompt": "type for Position", "completion":"Numeric(4.0).###"} +{ "prompt": "type for Hashtag", "completion":"VarChar(40).###"} +{ "prompt": "type for Duration", "completion":"Numeric(3.0).###"} +{ "prompt": "type for ColorTrack", "completion":"Character(3).###"} +{ "prompt": "type for Description", "completion":"LongVarChar(2M).###"} +{ "prompt": "type for SponsorType", "completion":"Character(1).###"} +{ "prompt": "type for Count", "completion":"Numeric(4.0).###"} +{ "prompt": "type for ListType", "completion":"Character(1).###"} From 63befd4ab430cc46b827ab704349d730df056315 Mon Sep 17 00:00:00 2001 From: "mike@gotmike.com" Date: Tue, 31 Jan 2023 20:30:29 -0500 Subject: [PATCH 2/9] remove unused usings; add missing properties; --- OpenAI_API/Model/Model.cs | 195 +++++++++++++++++++++++++++++--------- 1 file changed, 152 insertions(+), 43 deletions(-) diff --git a/OpenAI_API/Model/Model.cs b/OpenAI_API/Model/Model.cs index 1d321a4..683b82c 100644 --- a/OpenAI_API/Model/Model.cs +++ b/OpenAI_API/Model/Model.cs @@ -1,22 +1,19 @@ using Newtonsoft.Json; -using System; using System.Collections.Generic; -using System.Net.Http; -using System.Text; using System.Threading.Tasks; namespace OpenAI_API { - /// - /// Represents a language model - /// - public class Model - { - /// - /// The id/name of the model - /// - [JsonProperty("id")] - public string ModelID { get; set; } + /// + /// Represents a language model + /// + public class Model + { + /// + /// The id/name of the model + /// + [JsonProperty("id")] + public string ModelID { get; set; } /// /// The owner of this model. Generally "openai" is a generic OpenAI model, or the organization if a custom or finetuned model. @@ -24,41 +21,71 @@ public class Model [JsonProperty("owned_by")] public string OwnedBy { get; set; } + /// + /// The type of object. Should always be 'model'. + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// + /// The owner of this model. Generally "openai" is a generic OpenAI model, or the organization if a custom or finetuned model. + /// + [JsonProperty("created")] + public long Created { get; set; } + + /// + /// Permissions for use of the model + /// + [JsonProperty("permission")] + public List Permission { get; set; } = new List(); + + /// + /// Currently (2023-01-27) seems like this is duplicate of but including for completeness. + /// + [JsonProperty("root")] + public string Root { get; set; } + + /// + /// Currently (2023-01-27) seems unused, probably intended for nesting of models in a later release + /// + [JsonProperty("parent")] + public string Parent { get; set; } + /// /// Allows an model to be implicitly cast to the string of its /// /// The to cast to a string. public static implicit operator string(Model model) - { - return model?.ModelID; - } + { + return model?.ModelID; + } - /// - /// Allows a string to be implicitly cast as an with that - /// - /// The id/ to use - public static implicit operator Model(string name) - { - return new Model(name); - } + /// + /// Allows a string to be implicitly cast as an with that + /// + /// The id/ to use + public static implicit operator Model(string name) + { + return new Model(name); + } - /// - /// Represents an Model with the given id/ - /// - /// The id/ to use. - /// - public Model(string name) - { - this.ModelID = name; - } + /// + /// Represents an Model with the given id/ + /// + /// The id/ to use. + /// + public Model(string name) + { + this.ModelID = name; + } - /// - /// Represents a generic Model/model - /// - public Model() - { + /// + /// Represents a generic Model/model + /// + public Model() + { - } + } @@ -105,8 +132,90 @@ public Model() /// API authentication in order to call the API endpoint. If not specified, attempts to use a default. /// Asynchronously returns an Model with all relevant properties filled in public async Task RetrieveModelDetailsAsync(APIAuthentication auth = null) - { - return await ModelsEndpoint.RetrieveModelDetailsAsync(this.ModelID, auth); - } - } + { + return await ModelsEndpoint.RetrieveModelDetailsAsync(this.ModelID, auth); + } + } + + /// + /// Permissions for using the model + /// + public class Permissions + { + /// + /// Permission Id (not to be confused with ModelId + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Object type, should always be 'model_permission' + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// + /// Unix timestamp for creation date/time + /// + [JsonProperty("created")] + public long Created { get; set; } + + /// + /// Can the engine (model?) be created? + /// + [JsonProperty("allow_create_engine")] + public bool AllowCreateEngine { get; set; } + + /// + /// Does the model support temperature sampling? + /// https://beta.openai.com/docs/api-reference/completions/create#completions/create-temperature + /// + [JsonProperty("allow_sampling")] + public bool AllowSampling { get; set; } + + /// + /// Does the model support logprobs? + /// https://beta.openai.com/docs/api-reference/completions/create#completions/create-logprobs + /// + [JsonProperty("allow_logprobs")] + public bool AllowLogProbs { get; set; } + + /// + /// Does the model support search indices? + /// + [JsonProperty("allow_search_indices")] + public bool AllowSearchIndices { get; set; } + + /// + /// ?? + /// + [JsonProperty("allow_view")] + public bool AllowView { get; set; } + + /// + /// Does the model allow fine tuning? + /// https://beta.openai.com/docs/api-reference/fine-tunes + /// + [JsonProperty("allow_fine_tuning")] + public bool AllowFineTuning { get; set; } + + /// + /// Is the model only allowed for a particular organization? Seems not implemented yet. Always '*'. + /// + [JsonProperty("organization")] + public string Organization { get; set; } + + /// + /// Is the model part of a group? Seems not implemented yet. Always null. + /// + [JsonProperty("group")] + public string Group { get; set; } + + /// + /// ?? + /// + [JsonProperty("is_blocking")] + public bool IsBlocking { get; set; } + } + } From 133d533abb2dd37a1c769715a70a3c50577dadd9 Mon Sep 17 00:00:00 2001 From: BigMike Date: Tue, 31 Jan 2023 21:55:35 -0500 Subject: [PATCH 3/9] updating newtonsoft to 13.0.2 --- OpenAI_API/OpenAI_API.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAI_API/OpenAI_API.csproj b/OpenAI_API/OpenAI_API.csproj index 85c8c9e..aa84576 100644 --- a/OpenAI_API/OpenAI_API.csproj +++ b/OpenAI_API/OpenAI_API.csproj @@ -34,7 +34,7 @@ - + From e76fbe60a84c4c80e99ba41a8df84570187396c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 23:52:14 +0000 Subject: [PATCH 4/9] Bump Newtonsoft.Json from 12.0.3 to 13.0.1 in /OpenAI_API Bumps [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json) from 12.0.3 to 13.0.1. - [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases) - [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/12.0.3...13.0.1) --- updated-dependencies: - dependency-name: Newtonsoft.Json dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- OpenAI_API/OpenAI_API.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAI_API/OpenAI_API.csproj b/OpenAI_API/OpenAI_API.csproj index 85c8c9e..e328991 100644 --- a/OpenAI_API/OpenAI_API.csproj +++ b/OpenAI_API/OpenAI_API.csproj @@ -34,7 +34,7 @@ - + From ae5e51bbe377ec9a7114d7a540bbe239131d4380 Mon Sep 17 00:00:00 2001 From: Roger Date: Thu, 2 Feb 2023 14:31:30 -0800 Subject: [PATCH 5/9] Update Newtonsoft.Json package due to security concern --- OpenAI_API/OpenAI_API.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAI_API/OpenAI_API.csproj b/OpenAI_API/OpenAI_API.csproj index e328991..aa84576 100644 --- a/OpenAI_API/OpenAI_API.csproj +++ b/OpenAI_API/OpenAI_API.csproj @@ -34,7 +34,7 @@ - + From 44dac23bf65aeec6ba91c37685a8784e58a1920a Mon Sep 17 00:00:00 2001 From: Roger Date: Thu, 2 Feb 2023 17:09:03 -0800 Subject: [PATCH 6/9] Refactored all api endpoints, requests, and results to use inheritance. Thanks @gmilano for starting on this pattern when adding the files endpoint! --- OpenAI_API/APIAuthentication.cs | 68 +-- OpenAI_API/ApiResultBase.cs | 58 ++ OpenAI_API/BaseEndpoint.cs | 41 -- OpenAI_API/Completions/CompletionEndpoint.cs | 570 ++++++++----------- OpenAI_API/Completions/CompletionRequest.cs | 482 ++++++++-------- OpenAI_API/Completions/CompletionResult.cs | 36 +- OpenAI_API/EndpointBase.cs | 360 ++++++++++++ OpenAI_API/Files/File.cs | 27 +- OpenAI_API/Files/FilesEndpoint.cs | 93 ++- OpenAI_API/Model/Model.cs | 94 +-- OpenAI_API/Model/ModelsEndpoint.cs | 86 +-- OpenAI_API/OpenAIAPI.cs | 22 +- OpenAI_API/Search/SearchEndpoint.cs | 160 +++--- OpenAI_API/Search/SearchRequest.cs | 16 +- OpenAI_API/Search/SearchResponse.cs | 10 +- OpenAI_Tests/FilesEndpointTests.cs | 8 +- 16 files changed, 1142 insertions(+), 989 deletions(-) create mode 100644 OpenAI_API/ApiResultBase.cs delete mode 100644 OpenAI_API/BaseEndpoint.cs create mode 100644 OpenAI_API/EndpointBase.cs diff --git a/OpenAI_API/APIAuthentication.cs b/OpenAI_API/APIAuthentication.cs index d9fd4c0..709820a 100644 --- a/OpenAI_API/APIAuthentication.cs +++ b/OpenAI_API/APIAuthentication.cs @@ -17,10 +17,10 @@ public class APIAuthentication /// The API key, required to access the API endpoint. /// public string ApiKey { get; set; } - /// - /// The Organization ID to count API requests against. This can be found at https://beta.openai.com/account/org-settings. - /// - public string OpenAIOrganization { get; set; } + /// + /// The Organization ID to count API requests against. This can be found at https://beta.openai.com/account/org-settings. + /// + public string OpenAIOrganization { get; set; } /// /// Allows implicit casting from a string, so that a simple string API key can be provided in place of an instance of @@ -41,18 +41,18 @@ public APIAuthentication(string apiKey) } - /// - /// Instantiates a new Authentication object with the given , which may be . For users who belong to multiple organizations, you can specify which organization is used. Usage from these API requests will count against the specified organization's subscription quota. - /// - /// The API key, required to access the API endpoint. - /// The Organization ID to count API requests against. This can be found at https://beta.openai.com/account/org-settings. - public APIAuthentication(string apiKey, string openAIOrganization) - { - this.ApiKey = apiKey; + /// + /// Instantiates a new Authentication object with the given , which may be . For users who belong to multiple organizations, you can specify which organization is used. Usage from these API requests will count against the specified organization's subscription quota. + /// + /// The API key, required to access the API endpoint. + /// The Organization ID to count API requests against. This can be found at https://beta.openai.com/account/org-settings. + public APIAuthentication(string apiKey, string openAIOrganization) + { + this.ApiKey = apiKey; this.OpenAIOrganization = openAIOrganization; - } + } - private static APIAuthentication cachedDefault = null; + private static APIAuthentication cachedDefault = null; /// /// The default authentication to use when no other auth is specified. This can be set manually, or automatically loaded via environment variables or a config file. @@ -79,25 +79,25 @@ public static APIAuthentication Default } } - /// - /// Attempts to load api key from environment variables, as "OPENAI_KEY" or "OPENAI_API_KEY". Also loads org if from "OPENAI_ORGANIZATION" if present. - /// - /// Returns the loaded any api keys were found, or if there were no matching environment vars. - public static APIAuthentication LoadFromEnv() + /// + /// Attempts to load api key from environment variables, as "OPENAI_KEY" or "OPENAI_API_KEY". Also loads org if from "OPENAI_ORGANIZATION" if present. + /// + /// Returns the loaded any api keys were found, or if there were no matching environment vars. + public static APIAuthentication LoadFromEnv() { - string key = Environment.GetEnvironmentVariable("OPENAI_KEY"); + string key = Environment.GetEnvironmentVariable("OPENAI_KEY"); if (string.IsNullOrEmpty(key)) { - key = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); + key = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); if (string.IsNullOrEmpty(key)) - return null; + return null; } - string org = Environment.GetEnvironmentVariable("OPENAI_ORGANIZATION"); + string org = Environment.GetEnvironmentVariable("OPENAI_ORGANIZATION"); - return new APIAuthentication(key, org); + return new APIAuthentication(key, org); } /// @@ -128,16 +128,16 @@ public static APIAuthentication LoadFromPath(string directory = null, string fil { switch (parts[0].ToUpper()) { - case "OPENAI_KEY": - key = parts[1].Trim(); - break; - case "OPENAI_API_KEY": - key = parts[1].Trim(); - break; - case "OPENAI_ORGANIZATION": - org = parts[1].Trim(); - break; - default: + case "OPENAI_KEY": + key = parts[1].Trim(); + break; + case "OPENAI_API_KEY": + key = parts[1].Trim(); + break; + case "OPENAI_ORGANIZATION": + org = parts[1].Trim(); + break; + default: break; } } diff --git a/OpenAI_API/ApiResultBase.cs b/OpenAI_API/ApiResultBase.cs new file mode 100644 index 0000000..1895e5a --- /dev/null +++ b/OpenAI_API/ApiResultBase.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using System; + +namespace OpenAI_API +{ + /// + /// Represents a result from calling the OpenAI API, with all the common metadata returned from every endpoint + /// + abstract public class ApiResultBase + { + + /// The time when the result was generated + [JsonIgnore] + public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + + /// + /// The time when the result was generated in unix epoch format + /// + [JsonProperty("created")] + public int CreatedUnixTime { get; set; } + + /// + /// Which model was used to generate this result. + /// + [JsonProperty("model")] + public Model Model { get; set; } + + /// + /// Object type, ie: text_completion, file, fine-tune, list, etc + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// + /// The organization associated with the API request, as reported by the API. + /// + [JsonIgnore] + public string Organization { get; internal set; } + + /// + /// The server-side processing time as reported by the API. This can be useful for debugging where a delay occurs. + /// + [JsonIgnore] + public TimeSpan ProcessingTime { get; internal set; } + + /// + /// The request id of this API call, as reported in the response headers. This may be useful for troubleshooting or when contacting OpenAI support in reference to a specific request. + /// + [JsonIgnore] + public string RequestId { get; internal set; } + + /// + /// The Openai-Version used to generate this response, as reported in the response headers. This may be useful for troubleshooting or when contacting OpenAI support in reference to a specific request. + /// + [JsonIgnore] + public string OpenaiVersion { get; internal set; } + } +} \ No newline at end of file diff --git a/OpenAI_API/BaseEndpoint.cs b/OpenAI_API/BaseEndpoint.cs deleted file mode 100644 index d4a21a8..0000000 --- a/OpenAI_API/BaseEndpoint.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace OpenAI_API -{ - public abstract class BaseEndpoint - { - protected readonly OpenAIAPI api; - - public BaseEndpoint(OpenAIAPI api) - { - this.api = api; - } - - protected abstract string GetEndpoint(); - - protected string GetUrl() - { - return $"{OpenAIAPI.API_URL}{GetEndpoint()}"; - } - - protected HttpClient GetClient() - { - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", api.Auth?.ThisOrDefault().ApiKey); - client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); - return client; - } - - protected string GetErrorMessage(string resultAsString, HttpResponseMessage response, string name, string description = "") - { - return $"Error at {name} ( {description} ) with HTTP status code: {response.StatusCode} . Content: {resultAsString}"; - } - - - } -} diff --git a/OpenAI_API/Completions/CompletionEndpoint.cs b/OpenAI_API/Completions/CompletionEndpoint.cs index 37803e3..42945dc 100644 --- a/OpenAI_API/Completions/CompletionEndpoint.cs +++ b/OpenAI_API/Completions/CompletionEndpoint.cs @@ -11,344 +11,234 @@ namespace OpenAI_API { - /// - /// Text generation is the core function of the API. You give the API a prompt, and it generates a completion. The way you “program” the API to do a task is by simply describing the task in plain english or providing a few written examples. This simple approach works for a wide range of use cases, including summarization, translation, grammar correction, question answering, chatbots, composing emails, and much more (see the prompt library for inspiration). - /// - public class CompletionEndpoint - { - OpenAIAPI Api; - /// - /// This allows you to set default parameters for every request, for example to set a default temperature or max tokens. For every request, if you do not have a parameter set on the request but do have it set here as a default, the request will automatically pick up the default value. - /// - public CompletionRequest DefaultCompletionRequestArgs { get; set; } = new CompletionRequest() { Model = Model.DavinciText }; - - /// - /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . - /// - /// - internal CompletionEndpoint(OpenAIAPI api) - { - this.Api = api; - } - - #region Non-streaming - - /// - /// Ask the API to complete the prompt(s) using the specified request. This is non-streaming, so it will wait until the API returns the full result. - /// - /// The request to send to the API. This does not fall back to default values specified in . - /// Asynchronously returns the completion result. Look in its property for the completions. - public async Task CreateCompletionAsync(CompletionRequest request) - { - if (Api.Auth?.ApiKey is null) - { - throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); - } - - request.Stream = false; - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Api.Auth.ApiKey); - client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); - if (!string.IsNullOrEmpty(Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", Api.Auth.OpenAIOrganization); - - string jsonContent = JsonConvert.SerializeObject(request, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); - var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); - - var response = await client.PostAsync("https://api.openai.com/v1/completions", stringContent); - if (response.IsSuccessStatusCode) - { - string resultAsString = await response.Content.ReadAsStringAsync(); - - var res = JsonConvert.DeserializeObject(resultAsString); - try - { - res.Organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); - res.RequestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); - res.ProcessingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); - } - catch (Exception) { } - - - return res; - } - else - { - throw new HttpRequestException("Error calling OpenAi API to get completion. HTTP status code: " + response.StatusCode.ToString() + ". Request body: " + jsonContent); - } - } - - - /// - /// Ask the API to complete the prompt(s) using the specified request and a requested number of outputs. This is non-streaming, so it will wait until the API returns the full result. - /// - /// The request to send to the API. This does not fall back to default values specified in . - /// Overrides as a convenience. - /// Asynchronously returns the completion result. Look in its property for the completions, which should have a length equal to . - public Task CreateCompletionsAsync(CompletionRequest request, int numOutputs = 5) - { - request.NumChoicesPerPrompt = numOutputs; - return CreateCompletionAsync(request); - } - - /// - /// Ask the API to complete the prompt(s) using the specified parameters. This is non-streaming, so it will wait until the API returns the full result. Any non-specified parameters will fall back to default values specified in if present. - /// - /// The prompt to generate from - /// The model to use. You can use to see all of your available models, or use a standard model like . - /// How many tokens to complete to. Can return fewer if a stop sequence is hit. - /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. - /// 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. It is generally recommend to use this or but not both. - /// How many different choices to request for each prompt. - /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. - /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. - /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 10, the API will return a list of the 10 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. - /// Echo back the prompt in addition to the completion. - /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. - /// Asynchronously returns the completion result. Look in its property for the completions. - public Task CreateCompletionAsync(string prompt, - Model model = null, - int? max_tokens = null, - double? temperature = null, - double? top_p = null, - int? numOutputs = null, - double? presencePenalty = null, - double? frequencyPenalty = null, - int? logProbs = null, - bool? echo = null, - params string[] stopSequences - ) - { - CompletionRequest request = new CompletionRequest(DefaultCompletionRequestArgs) - { - Prompt = prompt, - Model = model ?? DefaultCompletionRequestArgs.Model, - MaxTokens = max_tokens ?? DefaultCompletionRequestArgs.MaxTokens, - Temperature = temperature ?? DefaultCompletionRequestArgs.Temperature, - TopP = top_p ?? DefaultCompletionRequestArgs.TopP, - NumChoicesPerPrompt = numOutputs ?? DefaultCompletionRequestArgs.NumChoicesPerPrompt, - PresencePenalty = presencePenalty ?? DefaultCompletionRequestArgs.PresencePenalty, - FrequencyPenalty = frequencyPenalty ?? DefaultCompletionRequestArgs.FrequencyPenalty, - Logprobs = logProbs ?? DefaultCompletionRequestArgs.Logprobs, - Echo = echo ?? DefaultCompletionRequestArgs.Echo, - MultipleStopSequences = stopSequences ?? DefaultCompletionRequestArgs.MultipleStopSequences - }; - return CreateCompletionAsync(request); - } - - /// - /// Ask the API to complete the prompt(s) using the specified promptes, with other paramets being drawn from default values specified in if present. This is non-streaming, so it will wait until the API returns the full result. - /// - /// One or more prompts to generate from - /// - public Task CreateCompletionAsync(params string[] prompts) - { - CompletionRequest request = new CompletionRequest(DefaultCompletionRequestArgs) - { - MultiplePrompts = prompts - }; - return CreateCompletionAsync(request); - } - - #endregion - - #region Streaming - - /// - /// Ask the API to complete the prompt(s) using the specified request, and stream the results to the as they come in. - /// If you are on the latest C# supporting async enumerables, you may prefer the cleaner syntax of instead. - /// - /// The request to send to the API. This does not fall back to default values specified in . - /// An action to be called as each new result arrives, which includes the index of the result in the overall result set. - public async Task StreamCompletionAsync(CompletionRequest request, Action resultHandler) - { - if (Api.Auth?.ApiKey is null) - { - throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); - } - - request = new CompletionRequest(request) { Stream = true }; - HttpClient client = new HttpClient(); - - string jsonContent = JsonConvert.SerializeObject(request, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); - var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); - - using (HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/completions")) - { - req.Content = stringContent; - req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Api.Auth.ApiKey); ; - req.Headers.Add("User-Agent", "okgodoit/dotnet_openai_api"); - - var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); - - if (response.IsSuccessStatusCode) - { - int index = 0; - - using (var stream = await response.Content.ReadAsStreamAsync()) - using (StreamReader reader = new StreamReader(stream)) - { - string line; - while ((line = await reader.ReadLineAsync()) != null) - { - if (line.StartsWith("data: ")) - line = line.Substring("data: ".Length); - if (line == "[DONE]") - { - return; - } - else if (!string.IsNullOrWhiteSpace(line)) - { - index++; - var res = JsonConvert.DeserializeObject(line.Trim()); - try - { - res.Organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); - res.RequestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); - res.ProcessingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); - } - catch (Exception) { } - - resultHandler(index, res); - } - } - } - } - else - { - throw new HttpRequestException("Error calling OpenAi API to get completion. HTTP status code: " + response.StatusCode.ToString() + ". Request body: " + jsonContent); - } - } - } - - /// - /// Ask the API to complete the prompt(s) using the specified request, and stream the results to the as they come in. - /// If you are on the latest C# supporting async enumerables, you may prefer the cleaner syntax of instead. - /// - /// The request to send to the API. This does not fall back to default values specified in . - /// An action to be called as each new result arrives. - public async Task StreamCompletionAsync(CompletionRequest request, Action resultHandler) - { - await StreamCompletionAsync(request, (i, res) => resultHandler(res)); - } - - /// - /// 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 async enumerables or if you are using the .NET Framework, you may need to use instead. - /// - /// The request to send to the API. This does not fall back to default values specified in . - /// An async enumerable with each of the results as they come in. See for more details on how to consume an async enumerable. - public async IAsyncEnumerable StreamCompletionEnumerableAsync(CompletionRequest request) - { - if (Api.Auth?.ApiKey is null) - { - throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); - } - - request = new CompletionRequest(request) { Stream = true }; - HttpClient client = new HttpClient(); - - string jsonContent = JsonConvert.SerializeObject(request, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); - var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); - - using (HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/completions")) - { - req.Content = stringContent; - req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Api.Auth.ApiKey); ; - req.Headers.Add("User-Agent", "okgodoit/dotnet_openai_api"); - - var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); - - if (response.IsSuccessStatusCode) - { - using (var stream = await response.Content.ReadAsStreamAsync()) - using (StreamReader reader = new StreamReader(stream)) - { - string line; - while ((line = await reader.ReadLineAsync()) != null) - { - if (line.StartsWith("data: ")) - line = line.Substring("data: ".Length); - if (line == "[DONE]") - { - yield break; - } - else if (!string.IsNullOrWhiteSpace(line)) - { - var res = JsonConvert.DeserializeObject(line.Trim()); - yield return res; - } - } - } - } - else - { - throw new HttpRequestException("Error calling OpenAi API to get completion. HTTP status code: " + response.StatusCode.ToString() + ". Request body: " + jsonContent); - } - } - } - - /// - /// Ask the API to complete the prompt(s) using the specified parameters. - /// Any non-specified parameters will fall back to default values specified in if present. - /// If you are not using C# 8 supporting async enumerables or if you are using the .NET Framework, you may need to use instead. - /// - /// The prompt to generate from - /// The model to use. You can use to see all of your available models, or use a standard model like . - /// How many tokens to complete to. Can return fewer if a stop sequence is hit. - /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. - /// 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. It is generally recommend to use this or but not both. - /// How many different choices to request for each prompt. - /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. - /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. - /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 10, the API will return a list of the 10 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. - /// Echo back the prompt in addition to the completion. - /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. - /// An async enumerable with each of the results as they come in. See the C# docs for more details on how to consume an async enumerable. - public IAsyncEnumerable StreamCompletionEnumerableAsync(string prompt, - Model model = null, - int? max_tokens = null, - double? temperature = null, - double? top_p = null, - int? numOutputs = null, - double? presencePenalty = null, - double? frequencyPenalty = null, - int? logProbs = null, - bool? echo = null, - params string[] stopSequences) - { - CompletionRequest request = new CompletionRequest(DefaultCompletionRequestArgs) - { - Prompt = prompt, - Model = model ?? DefaultCompletionRequestArgs.Model, - MaxTokens = max_tokens ?? DefaultCompletionRequestArgs.MaxTokens, - Temperature = temperature ?? DefaultCompletionRequestArgs.Temperature, - TopP = top_p ?? DefaultCompletionRequestArgs.TopP, - NumChoicesPerPrompt = numOutputs ?? DefaultCompletionRequestArgs.NumChoicesPerPrompt, - PresencePenalty = presencePenalty ?? DefaultCompletionRequestArgs.PresencePenalty, - FrequencyPenalty = frequencyPenalty ?? DefaultCompletionRequestArgs.FrequencyPenalty, - Logprobs = logProbs ?? DefaultCompletionRequestArgs.Logprobs, - Echo = echo ?? DefaultCompletionRequestArgs.Echo, - MultipleStopSequences = stopSequences ?? DefaultCompletionRequestArgs.MultipleStopSequences, - Stream = true - }; - return StreamCompletionEnumerableAsync(request); - } - #endregion - - #region Helpers - - /// - /// Simply returns a string of the prompt followed by the best completion - /// - /// The request to send to the API. This does not fall back to default values specified in . - /// A string of the prompt followed by the best completion - public async Task CreateAndFormatCompletion(CompletionRequest request) - { - string prompt = request.Prompt; - var result = await CreateCompletionAsync(request); - return prompt + result.ToString(); - } - - #endregion - } + /// + /// Text generation is the core function of the API. You give the API a prompt, and it generates a completion. The way you “program” the API to do a task is by simply describing the task in plain english or providing a few written examples. This simple approach works for a wide range of use cases, including summarization, translation, grammar correction, question answering, chatbots, composing emails, and much more (see the prompt library for inspiration). + /// + public class CompletionEndpoint : EndpointBase + { + /// + /// This allows you to set default parameters for every request, for example to set a default temperature or max tokens. For every request, if you do not have a parameter set on the request but do have it set here as a default, the request will automatically pick up the default value. + /// + public CompletionRequest DefaultCompletionRequestArgs { get; set; } = new CompletionRequest() { Model = Model.DavinciText }; + + /// + /// The name of the enpoint, which is the final path segment in the API URL. For example, "completions". + /// + protected override string Endpoint { get { return "completions"; } } + + /// + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// + /// + internal CompletionEndpoint(OpenAIAPI api) : base(api) { } + + #region Non-streaming + + /// + /// Ask the API to complete the prompt(s) using the specified request. This is non-streaming, so it will wait until the API returns the full result. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// Asynchronously returns the completion result. Look in its property for the completions. + public async Task CreateCompletionAsync(CompletionRequest request) + { + return await HttpPost(postData: request); + } + + /// + /// Ask the API to complete the prompt(s) using the specified request and a requested number of outputs. This is non-streaming, so it will wait until the API returns the full result. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// Overrides as a convenience. + /// Asynchronously returns the completion result. Look in its property for the completions, which should have a length equal to . + public Task CreateCompletionsAsync(CompletionRequest request, int numOutputs = 5) + { + request.NumChoicesPerPrompt = numOutputs; + return CreateCompletionAsync(request); + } + + /// + /// Ask the API to complete the prompt(s) using the specified parameters. This is non-streaming, so it will wait until the API returns the full result. Any non-specified parameters will fall back to default values specified in if present. + /// + /// The prompt to generate from + /// The model to use. You can use to see all of your available models, or use a standard model like . + /// How many tokens to complete to. Can return fewer if a stop sequence is hit. + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// 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. It is generally recommend to use this or but not both. + /// How many different choices to request for each prompt. + /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. + /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. + /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 10, the API will return a list of the 10 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. + /// Echo back the prompt in addition to the completion. + /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. + /// Asynchronously returns the completion result. Look in its property for the completions. + public Task CreateCompletionAsync(string prompt, + Model model = null, + int? max_tokens = null, + double? temperature = null, + double? top_p = null, + int? numOutputs = null, + double? presencePenalty = null, + double? frequencyPenalty = null, + int? logProbs = null, + bool? echo = null, + params string[] stopSequences + ) + { + CompletionRequest request = new CompletionRequest(DefaultCompletionRequestArgs) + { + Prompt = prompt, + Model = model ?? DefaultCompletionRequestArgs.Model, + MaxTokens = max_tokens ?? DefaultCompletionRequestArgs.MaxTokens, + Temperature = temperature ?? DefaultCompletionRequestArgs.Temperature, + TopP = top_p ?? DefaultCompletionRequestArgs.TopP, + NumChoicesPerPrompt = numOutputs ?? DefaultCompletionRequestArgs.NumChoicesPerPrompt, + PresencePenalty = presencePenalty ?? DefaultCompletionRequestArgs.PresencePenalty, + FrequencyPenalty = frequencyPenalty ?? DefaultCompletionRequestArgs.FrequencyPenalty, + Logprobs = logProbs ?? DefaultCompletionRequestArgs.Logprobs, + Echo = echo ?? DefaultCompletionRequestArgs.Echo, + MultipleStopSequences = stopSequences ?? DefaultCompletionRequestArgs.MultipleStopSequences + }; + return CreateCompletionAsync(request); + } + + /// + /// Ask the API to complete the prompt(s) using the specified promptes, with other paramets being drawn from default values specified in if present. This is non-streaming, so it will wait until the API returns the full result. + /// + /// One or more prompts to generate from + /// + public Task CreateCompletionAsync(params string[] prompts) + { + CompletionRequest request = new CompletionRequest(DefaultCompletionRequestArgs) + { + MultiplePrompts = prompts + }; + return CreateCompletionAsync(request); + } + + #endregion + + #region Streaming + + /// + /// Ask the API to complete the prompt(s) using the specified request, and stream the results to the as they come in. + /// If you are on the latest C# supporting async enumerables, you may prefer the cleaner syntax of instead. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// An action to be called as each new result arrives, which includes the index of the result in the overall result set. + public async Task StreamCompletionAsync(CompletionRequest request, Action resultHandler) + { + int index = 0; + + await foreach (var res in StreamCompletionEnumerableAsync(request)) + { + resultHandler(index++, res); + } + } + + /// + /// Ask the API to complete the prompt(s) using the specified request, and stream the results to the as they come in. + /// If you are on the latest C# supporting async enumerables, you may prefer the cleaner syntax of instead. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// An action to be called as each new result arrives. + public async Task StreamCompletionAsync(CompletionRequest request, Action resultHandler) + { + await foreach (var res in StreamCompletionEnumerableAsync(request)) + { + resultHandler(res); + } + } + + /// + /// 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 async enumerables or if you are using the .NET Framework, you may need to use instead. + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// An async enumerable with each of the results as they come in. See for more details on how to consume an async enumerable. + public IAsyncEnumerable StreamCompletionEnumerableAsync(CompletionRequest request) + { + request = new CompletionRequest(request) { Stream = true }; + return HttpStreamingRequest(Url, HttpMethod.Post, request); + } + + /// + /// Ask the API to complete the prompt(s) using the specified parameters. + /// Any non-specified parameters will fall back to default values specified in if present. + /// If you are not using C# 8 supporting async enumerables or if you are using the .NET Framework, you may need to use instead. + /// + /// The prompt to generate from + /// The model to use. You can use to see all of your available models, or use a standard model like . + /// How many tokens to complete to. Can return fewer if a stop sequence is hit. + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// 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. It is generally recommend to use this or but not both. + /// How many different choices to request for each prompt. + /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. + /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. + /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 10, the API will return a list of the 10 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. + /// Echo back the prompt in addition to the completion. + /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. + /// An async enumerable with each of the results as they come in. See the C# docs for more details on how to consume an async enumerable. + public IAsyncEnumerable StreamCompletionEnumerableAsync(string prompt, + Model model = null, + int? max_tokens = null, + double? temperature = null, + double? top_p = null, + int? numOutputs = null, + double? presencePenalty = null, + double? frequencyPenalty = null, + int? logProbs = null, + bool? echo = null, + params string[] stopSequences) + { + CompletionRequest request = new CompletionRequest(DefaultCompletionRequestArgs) + { + Prompt = prompt, + Model = model ?? DefaultCompletionRequestArgs.Model, + MaxTokens = max_tokens ?? DefaultCompletionRequestArgs.MaxTokens, + Temperature = temperature ?? DefaultCompletionRequestArgs.Temperature, + TopP = top_p ?? DefaultCompletionRequestArgs.TopP, + NumChoicesPerPrompt = numOutputs ?? DefaultCompletionRequestArgs.NumChoicesPerPrompt, + PresencePenalty = presencePenalty ?? DefaultCompletionRequestArgs.PresencePenalty, + FrequencyPenalty = frequencyPenalty ?? DefaultCompletionRequestArgs.FrequencyPenalty, + Logprobs = logProbs ?? DefaultCompletionRequestArgs.Logprobs, + Echo = echo ?? DefaultCompletionRequestArgs.Echo, + MultipleStopSequences = stopSequences ?? DefaultCompletionRequestArgs.MultipleStopSequences, + Stream = true + }; + return StreamCompletionEnumerableAsync(request); + } + #endregion + + #region Helpers + + /// + /// Simply returns a string of the prompt followed by the best completion + /// + /// The request to send to the API. This does not fall back to default values specified in . + /// A string of the prompt followed by the best completion + public async Task CreateAndFormatCompletion(CompletionRequest request) + { + string prompt = request.Prompt; + var result = await CreateCompletionAsync(request); + return prompt + result.ToString(); + } + + /// + /// Simply returns the best completion + /// + /// The prompt to complete + /// The best completion + public async Task GetCompletion(string prompt) + { + CompletionRequest request = new CompletionRequest(DefaultCompletionRequestArgs) + { + Prompt = prompt, + NumChoicesPerPrompt = 1 + }; + var result = await CreateCompletionAsync(request); + return result.ToString(); + } + + #endregion + } } diff --git a/OpenAI_API/Completions/CompletionRequest.cs b/OpenAI_API/Completions/CompletionRequest.cs index 583ca8f..423e677 100644 --- a/OpenAI_API/Completions/CompletionRequest.cs +++ b/OpenAI_API/Completions/CompletionRequest.cs @@ -9,246 +9,246 @@ namespace OpenAI_API { - /// - /// Represents a request to the Completions API. Mostly matches the parameters in the OpenAI docs, although some have been renames or expanded into single/multiple properties for ease of use. - /// - public class CompletionRequest - { - /// - /// ID of the model to use. You can use to see all of your available models, or use a standard model like . - /// - [JsonProperty("model")] - public string Model { get; set; } - - /// - /// This is only used for serializing the request into JSON, do not use it directly. - /// - [JsonProperty("prompt")] - public object CompiledPrompt - { - get - { - if (MultiplePrompts?.Length == 1) - return Prompt; - else - return MultiplePrompts; - } - } - - /// - /// If you are requesting more than one prompt, specify them as an array of strings. - /// - [JsonIgnore] - public string[] MultiplePrompts { get; set; } - - /// - /// For convenience, if you are only requesting a single prompt, set it here - /// - [JsonIgnore] - public string Prompt - { - get => MultiplePrompts.FirstOrDefault(); - set - { - MultiplePrompts = new string[] { value }; - } - } - - /// - /// The suffix that comes after a completion of inserted text. Defaults to null. - /// - [JsonProperty("suffix")] - public string Suffix { get; set; } - - /// - /// How many tokens to complete to. Can return fewer if a stop sequence is hit. Defaults to 16. - /// - [JsonProperty("max_tokens")] - public int? MaxTokens { get; set; } - - /// - /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. - /// - [JsonProperty("temperature")] - public double? Temperature { get; set; } - - /// - /// 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. It is generally recommend to use this or but not both. - /// - [JsonProperty("top_p")] - public double? TopP { get; set; } - - /// - /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. Defaults to 0. - /// - [JsonProperty("presence_penalty")] - public double? PresencePenalty { get; set; } - - /// - /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. Defaults to 0. - /// - [JsonProperty("frequency_penalty")] - public double? FrequencyPenalty { get; set; } - - /// - /// How many different choices to request for each prompt. Defaults to 1. - /// - [JsonProperty("n")] - public int? NumChoicesPerPrompt { get; set; } - - /// - /// Specifies where the results should stream and be returned at one time. Do not set this yourself, use the appropriate methods on instead. - /// - [JsonProperty("stream")] - public bool Stream { get; internal set; } = false; - - /// - /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 5, the API will return a list of the 5 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. The maximum value for logprobs is 5. - /// - [JsonProperty("logprobs")] - public int? Logprobs { get; set; } - - /// - /// Echo back the prompt in addition to the completion. Defaults to false. - /// - [JsonProperty("echo")] - public bool? Echo { get; set; } - - /// - /// This is only used for serializing the request into JSON, do not use it directly. - /// - [JsonProperty("stop")] - public object CompiledStop - { - get - { - if (MultipleStopSequences?.Length == 1) - return StopSequence; - else if (MultipleStopSequences?.Length > 0) - return MultipleStopSequences; - else - return null; - } - } - - /// - /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. - /// - [JsonIgnore] - public string[] MultipleStopSequences { get; set; } - - - /// - /// The stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence. For convenience, if you are only requesting a single stop sequence, set it here - /// - [JsonIgnore] - public string StopSequence - { - get => MultipleStopSequences?.FirstOrDefault() ?? null; - set - { - if (value != null) - MultipleStopSequences = new string[] { value }; - } - } - - /// - /// Generates best_of completions server-side and returns the "best" (the one with the highest log probability per token). Results cannot be streamed. - /// When used with n, best_of controls the number of candidate completions and n specifies how many to return – best_of must be greater than n. - /// Note: Because this parameter generates many completions, it can quickly consume your token quota.Use carefully and ensure that you have reasonable settings for max_tokens and stop. - /// - [JsonProperty("best_of")] - public int? BestOf { get; set; } - - /// - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. - /// - [JsonProperty("user")] - public string user { get; set; } - - /// - /// Cretes a new, empty - /// - public CompletionRequest() - { - - } - - /// - /// Creates a new , inheriting any parameters set in . - /// - /// The to copy - public CompletionRequest(CompletionRequest basedOn) - { - this.Model = basedOn.Model; - this.MultiplePrompts = basedOn.MultiplePrompts; - this.MaxTokens = basedOn.MaxTokens; - this.Temperature = basedOn.Temperature; - this.TopP = basedOn.TopP; - this.NumChoicesPerPrompt = basedOn.NumChoicesPerPrompt; - this.PresencePenalty = basedOn.PresencePenalty; - this.FrequencyPenalty = basedOn.FrequencyPenalty; - this.Logprobs = basedOn.Logprobs; - this.Echo = basedOn.Echo; - this.MultipleStopSequences = basedOn.MultipleStopSequences; - this.BestOf = basedOn.BestOf; - this.user = basedOn.user; - this.Suffix = basedOn.Suffix; - } - - /// - /// Creates a new , using the specified prompts - /// - /// One or more prompts to generate from - public CompletionRequest(params string[] prompts) - { - this.MultiplePrompts = prompts; - } - - /// - /// Creates a new with the specified parameters - /// - /// The prompt to generate from - /// The model to use. You can use to see all of your available models, or use a standard model like . - /// How many tokens to complete to. Can return fewer if a stop sequence is hit. - /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. - /// The suffix that comes after a completion of inserted text - /// 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. It is generally recommend to use this or but not both. - /// How many different choices to request for each prompt. - /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. - /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. - /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 10, the API will return a list of the 10 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. - /// Echo back the prompt in addition to the completion. - /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. - public CompletionRequest( - string prompt, - Model model = null, - int? max_tokens = null, - double? temperature = null, - string suffix = null, - double? top_p = null, - int? numOutputs = null, - double? presencePenalty = null, - double? frequencyPenalty = null, - int? logProbs = null, - bool? echo = null, - params string[] stopSequences) - { - this.Model = model; - this.Prompt = prompt; - this.MaxTokens = max_tokens; - this.Temperature = temperature; - this.Suffix = suffix; - this.TopP = top_p; - this.NumChoicesPerPrompt = numOutputs; - this.PresencePenalty = presencePenalty; - this.FrequencyPenalty = frequencyPenalty; - this.Logprobs = logProbs; - this.Echo = echo; - this.MultipleStopSequences = stopSequences; - } - - - } + /// + /// Represents a request to the Completions API. Mostly matches the parameters in the OpenAI docs, although some have been renames or expanded into single/multiple properties for ease of use. + /// + public class CompletionRequest + { + /// + /// ID of the model to use. You can use to see all of your available models, or use a standard model like . + /// + [JsonProperty("model")] + public string Model { get; set; } + + /// + /// This is only used for serializing the request into JSON, do not use it directly. + /// + [JsonProperty("prompt")] + public object CompiledPrompt + { + get + { + if (MultiplePrompts?.Length == 1) + return Prompt; + else + return MultiplePrompts; + } + } + + /// + /// If you are requesting more than one prompt, specify them as an array of strings. + /// + [JsonIgnore] + public string[] MultiplePrompts { get; set; } + + /// + /// For convenience, if you are only requesting a single prompt, set it here + /// + [JsonIgnore] + public string Prompt + { + get => MultiplePrompts.FirstOrDefault(); + set + { + MultiplePrompts = new string[] { value }; + } + } + + /// + /// The suffix that comes after a completion of inserted text. Defaults to null. + /// + [JsonProperty("suffix")] + public string Suffix { get; set; } + + /// + /// How many tokens to complete to. Can return fewer if a stop sequence is hit. Defaults to 16. + /// + [JsonProperty("max_tokens")] + public int? MaxTokens { get; set; } + + /// + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// + [JsonProperty("temperature")] + public double? Temperature { get; set; } + + /// + /// 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. It is generally recommend to use this or but not both. + /// + [JsonProperty("top_p")] + public double? TopP { get; set; } + + /// + /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. Defaults to 0. + /// + [JsonProperty("presence_penalty")] + public double? PresencePenalty { get; set; } + + /// + /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. Defaults to 0. + /// + [JsonProperty("frequency_penalty")] + public double? FrequencyPenalty { get; set; } + + /// + /// How many different choices to request for each prompt. Defaults to 1. + /// + [JsonProperty("n")] + public int? NumChoicesPerPrompt { get; set; } + + /// + /// Specifies where the results should stream and be returned at one time. Do not set this yourself, use the appropriate methods on instead. + /// + [JsonProperty("stream")] + public bool Stream { get; internal set; } = false; + + /// + /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 5, the API will return a list of the 5 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. The maximum value for logprobs is 5. + /// + [JsonProperty("logprobs")] + public int? Logprobs { get; set; } + + /// + /// Echo back the prompt in addition to the completion. Defaults to false. + /// + [JsonProperty("echo")] + public bool? Echo { get; set; } + + /// + /// This is only used for serializing the request into JSON, do not use it directly. + /// + [JsonProperty("stop")] + public object CompiledStop + { + get + { + if (MultipleStopSequences?.Length == 1) + return StopSequence; + else if (MultipleStopSequences?.Length > 0) + return MultipleStopSequences; + else + return null; + } + } + + /// + /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. + /// + [JsonIgnore] + public string[] MultipleStopSequences { get; set; } + + + /// + /// The stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence. For convenience, if you are only requesting a single stop sequence, set it here + /// + [JsonIgnore] + public string StopSequence + { + get => MultipleStopSequences?.FirstOrDefault() ?? null; + set + { + if (value != null) + MultipleStopSequences = new string[] { value }; + } + } + + /// + /// Generates best_of completions server-side and returns the "best" (the one with the highest log probability per token). Results cannot be streamed. + /// When used with n, best_of controls the number of candidate completions and n specifies how many to return – best_of must be greater than n. + /// Note: Because this parameter generates many completions, it can quickly consume your token quota.Use carefully and ensure that you have reasonable settings for max_tokens and stop. + /// + [JsonProperty("best_of")] + public int? BestOf { get; set; } + + /// + /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. + /// + [JsonProperty("user")] + public string user { get; set; } + + /// + /// Cretes a new, empty + /// + public CompletionRequest() + { + + } + + /// + /// Creates a new , inheriting any parameters set in . + /// + /// The to copy + public CompletionRequest(CompletionRequest basedOn) + { + this.Model = basedOn.Model; + this.MultiplePrompts = basedOn.MultiplePrompts; + this.MaxTokens = basedOn.MaxTokens; + this.Temperature = basedOn.Temperature; + this.TopP = basedOn.TopP; + this.NumChoicesPerPrompt = basedOn.NumChoicesPerPrompt; + this.PresencePenalty = basedOn.PresencePenalty; + this.FrequencyPenalty = basedOn.FrequencyPenalty; + this.Logprobs = basedOn.Logprobs; + this.Echo = basedOn.Echo; + this.MultipleStopSequences = basedOn.MultipleStopSequences; + this.BestOf = basedOn.BestOf; + this.user = basedOn.user; + this.Suffix = basedOn.Suffix; + } + + /// + /// Creates a new , using the specified prompts + /// + /// One or more prompts to generate from + public CompletionRequest(params string[] prompts) + { + this.MultiplePrompts = prompts; + } + + /// + /// Creates a new with the specified parameters + /// + /// The prompt to generate from + /// The model to use. You can use to see all of your available models, or use a standard model like . + /// How many tokens to complete to. Can return fewer if a stop sequence is hit. + /// What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. It is generally recommend to use this or but not both. + /// The suffix that comes after a completion of inserted text + /// 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. It is generally recommend to use this or but not both. + /// How many different choices to request for each prompt. + /// The scale of the penalty applied if a token is already present at all. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. + /// The scale of the penalty for how often a token is used. Should generally be between 0 and 1, although negative numbers are allowed to encourage token reuse. + /// Include the log probabilities on the logprobs most likely tokens, which can be found in -> . So for example, if logprobs is 10, the API will return a list of the 10 most likely tokens. If logprobs is supplied, the API will always return the logprob of the sampled token, so there may be up to logprobs+1 elements in the response. + /// Echo back the prompt in addition to the completion. + /// One or more sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. + public CompletionRequest( + string prompt, + Model model = null, + int? max_tokens = null, + double? temperature = null, + string suffix = null, + double? top_p = null, + int? numOutputs = null, + double? presencePenalty = null, + double? frequencyPenalty = null, + int? logProbs = null, + bool? echo = null, + params string[] stopSequences) + { + this.Model = model; + this.Prompt = prompt; + this.MaxTokens = max_tokens; + this.Temperature = temperature; + this.Suffix = suffix; + this.TopP = top_p; + this.NumChoicesPerPrompt = numOutputs; + this.PresencePenalty = presencePenalty; + this.FrequencyPenalty = frequencyPenalty; + this.Logprobs = logProbs; + this.Echo = echo; + this.MultipleStopSequences = stopSequences; + } + + + } } diff --git a/OpenAI_API/Completions/CompletionResult.cs b/OpenAI_API/Completions/CompletionResult.cs index f53348f..dadefba 100644 --- a/OpenAI_API/Completions/CompletionResult.cs +++ b/OpenAI_API/Completions/CompletionResult.cs @@ -48,7 +48,7 @@ public override string ToString() /// /// Represents a result from calling the Completion API /// - public class CompletionResult + public class CompletionResult : ApiResultBase { /// /// The identifier of the result, which may be used during troubleshooting @@ -56,46 +56,12 @@ public class CompletionResult [JsonProperty("id")] public string Id { get; set; } - /// - /// The time when the result was generated in unix epoch format - /// - [JsonProperty("created")] - public int CreatedUnixTime { get; set; } - - /// The time when the result was generated - [JsonIgnore] - public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; - - /// - /// Which model was used to generate this result. - /// - [JsonProperty("model")] - public Model Model { get; set; } - /// /// The completions returned by the API. Depending on your request, there may be 1 or many choices. /// [JsonProperty("choices")] public List Completions { get; set; } - /// - /// The server-side processing time as reported by the API. This can be useful for debugging where a delay occurs. - /// - [JsonIgnore] - public TimeSpan ProcessingTime { get; set; } - - /// - /// The organization associated with the API request, as reported by the API. - /// - [JsonIgnore] - public string Organization{ get; set; } - - /// - /// The request id of this API call, as reported in the response headers. This may be useful for troubleshooting or when contacting OpenAI support in reference to a specific request. - /// - [JsonIgnore] - public string RequestId { get; set; } - /// /// Gets the text of the first completion, representing the main result diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs new file mode 100644 index 0000000..add1a1d --- /dev/null +++ b/OpenAI_API/EndpointBase.cs @@ -0,0 +1,360 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Security.Authentication; +using System.Text; +using System.Threading.Tasks; + +namespace OpenAI_API +{ + /// + /// A base object for any OpenAI API enpoint, encompassing common functionality + /// + public abstract class EndpointBase + { + /// + /// + /// + protected readonly OpenAIAPI Api; + + /// + /// Constructor of the api endpoint base, to be called from the contructor of any devived classes. Rather than instantiating any endpoint yourself, access it through an instance of . + /// + /// + internal EndpointBase(OpenAIAPI api) + { + this.Api = api; + } + + /// + /// The name of the enpoint, which is the final path segment in the API URL. Must be overriden in a derived class. + /// + protected abstract string Endpoint { get; } + + /// + /// Gets the URL of the endpoint, based on the base OpenAI API URL followed by the endpoint name. For example "https://api.openai.com/v1/completions" + /// + protected string Url + { + get + { + return $"{Api.ApiUrlBase}{Endpoint}"; + } + } + + /// + /// Gets an HTTPClient with the appropriate authorization and other headers set + /// + /// The fully initialized HttpClient + /// Thrown if there is no valid authentication. Please refer to for details. + protected HttpClient GetClient() + { + if (Api.Auth?.ApiKey is null) + { + throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); + } + + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Api.Auth.ApiKey); + client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); + if (!string.IsNullOrEmpty(Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", Api.Auth.OpenAIOrganization); + + return client; + } + + /// + /// Formats a human-readable error message relating to calling the API and parsing the response + /// + /// The full content returned in the http response + /// The http response object itself + /// The name of the endpoint being used + /// Additional details about the endpoint of this request (optional) + /// A human-readable string error message. + protected string GetErrorMessage(string resultAsString, HttpResponseMessage response, string name, string description = "") + { + return $"Error at {name} ({description}) with HTTP status code: {response.StatusCode}. Content: {resultAsString ?? ""}"; + } + + + /// + /// Sends an HTTP request and returns the response. Does not do any parsing, but does do error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// (optional) If true, streams the response. Otherwise waits for the entire response before returning. + /// The HttpResponseMessage of the response, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + private async Task HttpRequestRaw(string url = null, HttpMethod verb = null, object postData = null, bool streaming = false) + { + if (string.IsNullOrEmpty(url)) + url = this.Url; + + if (verb == null) + verb = HttpMethod.Get; + + var client = GetClient(); + + HttpResponseMessage response = null; + string resultAsString = null; + HttpRequestMessage req = new HttpRequestMessage(verb, url); + + if (postData != null) + { + if (postData is HttpContent) + { + req.Content = postData as HttpContent; + } + else + { + string jsonContent = JsonConvert.SerializeObject(postData, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); + var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + response = await client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + + if (response.IsSuccessStatusCode) + { + return response; + } + else + { + try + { + resultAsString = await response.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + resultAsString = "Additionally, the following error was thrown when attemping to read the response content: " + e.ToString(); + } + + throw new HttpRequestException(GetErrorMessage(resultAsString, response, Endpoint, url)); + } + } + + /// + /// Sends an HTTP Get request and return the string content of the response without parsing, and does error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// The text string of the response, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + internal async Task HttpGetContent(string url = null) + { + var response = await HttpRequestRaw(url); + return await response.Content.ReadAsStringAsync(); + } + + + /// + /// Sends an HTTP Request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + private async Task HttpRequest(string url = null, HttpMethod verb = null, object postData = null) where T : ApiResultBase + { + var response = await HttpRequestRaw(url, verb, postData); + string resultAsString = await response.Content.ReadAsStringAsync(); + + var res = JsonConvert.DeserializeObject(resultAsString); + try + { + res.Organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); + res.RequestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); + res.ProcessingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); + res.OpenaiVersion = response.Headers.GetValues("Openai-Version").FirstOrDefault(); + if (string.IsNullOrEmpty(res.Model)) + res.Model = response.Headers.GetValues("Openai-Model").FirstOrDefault(); + } + catch (Exception e) + { + Debug.Print($"Issue parsing metadata of OpenAi Response. Url: {url}, Error: {e.ToString()}, Response: {resultAsString}. This is probably ignorable."); + } + + return res; + } + + /* + /// + /// Sends an HTTP Request, supporting a streaming response + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + private async Task StreamingHttpRequest(string url = null, HttpMethod verb = null, object postData = null) where T : ApiResultBase + { + var response = await HttpRequestRaw(url, verb, postData); + string resultAsString = await response.Content.ReadAsStringAsync(); + + var res = JsonConvert.DeserializeObject(resultAsString); + try + { + res.Organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); + res.RequestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); + res.ProcessingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); + res.OpenaiVersion = response.Headers.GetValues("Openai-Version").FirstOrDefault(); + if (string.IsNullOrEmpty(res.Model)) + res.Model = response.Headers.GetValues("Openai-Model").FirstOrDefault(); + } + catch (Exception e) + { + Debug.Print($"Issue parsing metadata of OpenAi Response. Url: {url}, Error: {e.ToString()}, Response: {resultAsString}. This is probably ignorable."); + } + + return res; + } + */ + + /// + /// Sends an HTTP Get request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpGet(string url = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Get); + } + + /// + /// Sends an HTTP Post request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpPost(string url = null, object postData = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Post, postData); + } + + /// + /// Sends an HTTP Delete request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpDelete(string url = null, object postData = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Delete, postData); + } + + + /// + /// Sends an HTTP Put request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpPut(string url = null, object postData = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Put, postData); + } + + + + /* + /// + /// Sends an HTTP request and handles a streaming response. Does basic line splitting and error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// The HttpResponseMessage of the response, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + private async IAsyncEnumerable HttpStreamingRequestRaw(string url = null, HttpMethod verb = null, object postData = null) + { + var response = await HttpRequestRaw(url, verb, postData, true); + + using (var stream = await response.Content.ReadAsStreamAsync()) + using (StreamReader reader = new StreamReader(stream)) + { + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + if (line.StartsWith("data: ")) + line = line.Substring("data: ".Length); + if (line == "[DONE]") + { + yield break; + } + else if (!string.IsNullOrWhiteSpace(line)) + { + yield return line.Trim(); + } + } + } + } + */ + + + /// + /// Sends an HTTP request and handles a streaming response. Does basic line splitting and error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// The HttpResponseMessage of the response, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + protected async IAsyncEnumerable HttpStreamingRequest(string url = null, HttpMethod verb = null, object postData = null) where T : ApiResultBase + { + var response = await HttpRequestRaw(url, verb, postData, true); + + string resultAsString = ""; + + using (var stream = await response.Content.ReadAsStreamAsync()) + using (StreamReader reader = new StreamReader(stream)) + { + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + resultAsString += line + Environment.NewLine; + + if (line.StartsWith("data: ")) + line = line.Substring("data: ".Length); + if (line == "[DONE]") + { + yield break; + } + else if (!string.IsNullOrWhiteSpace(line)) + { + var res = JsonConvert.DeserializeObject(line); + try + { + res.Organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); + res.RequestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); + res.ProcessingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); + res.OpenaiVersion = response.Headers.GetValues("Openai-Version").FirstOrDefault(); + if (string.IsNullOrEmpty(res.Model)) + res.Model = response.Headers.GetValues("Openai-Model").FirstOrDefault(); + } + catch (Exception e) + { + Debug.Print($"Issue parsing metadata of OpenAi Response. Url: {url}, Error: {e.ToString()}, Response: {resultAsString}. This is probably ignorable."); + } + + yield return res; + } + } + } + } + } +} diff --git a/OpenAI_API/Files/File.cs b/OpenAI_API/Files/File.cs index e6054e7..1eb8c60 100644 --- a/OpenAI_API/Files/File.cs +++ b/OpenAI_API/Files/File.cs @@ -7,39 +7,35 @@ namespace OpenAI_API.Files { - public class File + /// + /// Represents a single file used with the OpenAI Files endpoint. Files are used to upload and manage documents that can be used with features like Fine-tuning. + /// + public class File : ApiResultBase { /// - /// unique id for this file, so that it can be referenced in other operations + /// Unique id for this file, so that it can be referenced in other operations /// [JsonProperty("id")] public string Id { get; set; } /// - /// the name of the file + /// The name of the file /// [JsonProperty("filename")] public string Name { get; set; } - /// /// What is the purpose of this file, fine-tune, search, etc /// - [JsonProperty("Purpose")] + [JsonProperty("purpose")] public string Purpose { get; set; } - - /// - /// object type, ie: file, image, etc - /// - [JsonProperty("object")] - public string Object { get; set; } - + /// /// The size of the file in bytes /// [JsonProperty("bytes")] public long Bytes { get; set; } - + /// /// Timestamp for the creation time of this file /// @@ -47,12 +43,11 @@ public class File public long CreatedAt { get; set; } /// - /// The object was deleted, this attribute is used in the Delete file operation + /// When the object is deleted, this attribute is used in the Delete file operation /// [JsonProperty("deleted")] public bool Deleted { get; set; } - /// /// The status of the File (ie when an upload operation was done: "uploaded") /// @@ -64,6 +59,6 @@ public class File /// [JsonProperty("status_details")] public string StatusDetails { get; set; } - + } } diff --git a/OpenAI_API/Files/FilesEndpoint.cs b/OpenAI_API/Files/FilesEndpoint.cs index 5a93a13..44c744e 100644 --- a/OpenAI_API/Files/FilesEndpoint.cs +++ b/OpenAI_API/Files/FilesEndpoint.cs @@ -13,11 +13,18 @@ namespace OpenAI_API.Files /// /// The API endpoint for operations List, Upload, Delete, Retrieve files /// - public class FilesEndpoint : BaseEndpoint + public class FilesEndpoint : EndpointBase { - public FilesEndpoint(OpenAIAPI api) : base(api) {} + /// + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// + /// + internal FilesEndpoint(OpenAIAPI api) : base(api) { } - protected override string GetEndpoint() { return "files"; } + /// + /// The name of the enpoint, which is the final path segment in the API URL. For example, "files". + /// + protected override string Endpoint { get { return "files"; } } /// /// Get the list of all files @@ -26,15 +33,7 @@ public FilesEndpoint(OpenAIAPI api) : base(api) {} /// public async Task> GetFilesAsync() { - var response = await GetClient().GetAsync(GetUrl()); - string resultAsString = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - var files = JsonConvert.DeserializeObject(resultAsString).Data; - return files; - } - throw new HttpRequestException(GetErrorMessage(resultAsString, response, "List files", "Get the list of all files")); + return (await HttpGet()).Data; } /// @@ -44,15 +43,7 @@ public async Task> GetFilesAsync() /// public async Task GetFileAsync(string fileId) { - var response = await GetClient().GetAsync($"{GetUrl()}/{fileId}"); - string resultAsString = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - var file = JsonConvert.DeserializeObject(resultAsString); - return file; - } - throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Retrieve file")); + return await HttpGet($"{Url}/{fileId}"); } @@ -63,15 +54,7 @@ public async Task GetFileAsync(string fileId) /// public async Task GetFileContentAsStringAsync(string fileId) { - var response = await GetClient().GetAsync($"{GetUrl()}/{fileId}/content"); - string resultAsString = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - return resultAsString; - - } - throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Retrieve file content")); + return await HttpGetContent($"{Url}/{fileId}/content"); } /// @@ -81,48 +64,38 @@ public async Task GetFileContentAsStringAsync(string fileId) /// public async Task DeleteFileAsync(string fileId) { - var response = await GetClient().DeleteAsync($"{GetUrl()}/{fileId}"); - string resultAsString = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - var file = JsonConvert.DeserializeObject(resultAsString); - return file; - } - throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Delete file")); + return await HttpDelete($"{Url}/{fileId}"); } + /// - /// Upload a file that contains document(s) to be used across various endpoints/features. Currently, the size of all the files uploaded by one organization can be up to 1 GB. Please contact us if you need to increase the storage limit + /// Upload a file that contains document(s) to be used across various endpoints/features. Currently, the size of all the files uploaded by one organization can be up to 1 GB. Please contact OpenAI if you need to increase the storage limit /// - /// The name of the file to use for this request + /// The name of the file to use for this request /// The intendend purpose of the uploaded documents. Use "fine-tune" for Fine-tuning. This allows us to validate the format of the uploaded file. - public async Task UploadFileAsync(string file, string purpose = "fine-tune") + public async Task UploadFileAsync(string filePath, string purpose = "fine-tune") { HttpClient client = GetClient(); var content = new MultipartFormDataContent { { new StringContent(purpose), "purpose" }, - { new ByteArrayContent(System.IO.File.ReadAllBytes(file)), "file", Path.GetFileName(file) } + { new ByteArrayContent(System.IO.File.ReadAllBytes(filePath)), "file", Path.GetFileName(filePath) } }; - var response = await client.PostAsync($"{GetUrl()}", content); - string resultAsString = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(resultAsString); - } - throw new HttpRequestException(GetErrorMessage(resultAsString, response, "Upload file", "Upload a file that contains document(s) to be used across various endpoints/features.")); + return await HttpPost(Url, content); + } + + /// + /// A helper class to deserialize the JSON API responses. This should not be used directly. + /// + private class FilesData : ApiResultBase + { + [JsonProperty("data")] + public List Data { get; set; } + [JsonProperty("object")] + public string Obj { get; set; } } } - /// - /// A helper class to deserialize the JSON API responses. This should not be used directly. - /// - class FilesData - { - [JsonProperty("data")] - public List Data { get; set; } - [JsonProperty("object")] - public string Obj { get; set; } - } + + } diff --git a/OpenAI_API/Model/Model.cs b/OpenAI_API/Model/Model.cs index 1d321a4..e0ee2bd 100644 --- a/OpenAI_API/Model/Model.cs +++ b/OpenAI_API/Model/Model.cs @@ -18,17 +18,17 @@ public class Model [JsonProperty("id")] public string ModelID { get; set; } - /// - /// The owner of this model. Generally "openai" is a generic OpenAI model, or the organization if a custom or finetuned model. - /// - [JsonProperty("owned_by")] - public string OwnedBy { get; set; } - - /// - /// Allows an model to be implicitly cast to the string of its - /// - /// The to cast to a string. - public static implicit operator string(Model model) + /// + /// The owner of this model. Generally "openai" is a generic OpenAI model, or the organization if a custom or finetuned model. + /// + [JsonProperty("owned_by")] + public string OwnedBy { get; set; } + + /// + /// Allows an model to be implicitly cast to the string of its + /// + /// The to cast to a string. + public static implicit operator string(Model model) { return model?.ModelID; } @@ -62,51 +62,51 @@ public Model() - /// - /// Capable of very simple tasks, usually the fastest model in the GPT-3 series, and lowest cost - /// - public static Model AdaText => new Model("text-ada-001") { OwnedBy = "openai" }; + /// + /// Capable of very simple tasks, usually the fastest model in the GPT-3 series, and lowest cost + /// + public static Model AdaText => new Model("text-ada-001") { OwnedBy = "openai" }; - /// - /// Capable of straightforward tasks, very fast, and lower cost. - /// - public static Model BabbageText => new Model("text-babbage-001") { OwnedBy = "openai" }; + /// + /// Capable of straightforward tasks, very fast, and lower cost. + /// + public static Model BabbageText => new Model("text-babbage-001") { OwnedBy = "openai" }; - /// - /// Very capable, but faster and lower cost than Davinci. - /// - public static Model CurieText => new Model("text-curie-001") { OwnedBy = "openai" }; + /// + /// Very capable, but faster and lower cost than Davinci. + /// + public static Model CurieText => new Model("text-curie-001") { OwnedBy = "openai" }; - /// - /// Most capable GPT-3 model. Can do any task the other models can do, often with higher quality, longer output and better instruction-following. Also supports inserting completions within text. - /// - public static Model DavinciText => new Model("text-davinci-003") { OwnedBy = "openai" }; + /// + /// Most capable GPT-3 model. Can do any task the other models can do, often with higher quality, longer output and better instruction-following. Also supports inserting completions within text. + /// + public static Model DavinciText => new Model("text-davinci-003") { OwnedBy = "openai" }; - /// - /// Almost as capable as Davinci Codex, but slightly faster. This speed advantage may make it preferable for real-time applications. - /// - public static Model CushmanCode => new Model("code-cushman-001") { OwnedBy = "openai" }; + /// + /// Almost as capable as Davinci Codex, but slightly faster. This speed advantage may make it preferable for real-time applications. + /// + public static Model CushmanCode => new Model("code-cushman-001") { OwnedBy = "openai" }; - /// - /// Most capable Codex model. Particularly good at translating natural language to code. In addition to completing code, also supports inserting completions within code. - /// - public static Model DavinciCode => new Model("code-davinci-002") { OwnedBy = "openai" }; + /// + /// Most capable Codex model. Particularly good at translating natural language to code. In addition to completing code, also supports inserting completions within code. + /// + public static Model DavinciCode => new Model("code-davinci-002") { OwnedBy = "openai" }; - /// - /// OpenAI offers one second-generation embedding model for use with the embeddings API endpoint. - /// - public static Model AdaTextEmbedding => new Model("text-embedding-ada-002") { OwnedBy = "openai" }; + /// + /// OpenAI offers one second-generation embedding model for use with the embeddings API endpoint. + /// + public static Model AdaTextEmbedding => new Model("text-embedding-ada-002") { OwnedBy = "openai" }; - /// - /// Gets more details about this Model from the API, specifically properties such as and permissions. - /// - /// API authentication in order to call the API endpoint. If not specified, attempts to use a default. - /// Asynchronously returns an Model with all relevant properties filled in - public async Task RetrieveModelDetailsAsync(APIAuthentication auth = null) + /// + /// Gets more details about this Model from the API, specifically properties such as and permissions. + /// + /// An instance of the API with authentication in order to call the endpoint. + /// Asynchronously returns an Model with all relevant properties filled in + public async Task RetrieveModelDetailsAsync(OpenAI_API.OpenAIAPI api) { - return await ModelsEndpoint.RetrieveModelDetailsAsync(this.ModelID, auth); - } + return await api.Models.RetrieveModelDetailsAsync(this.ModelID); + } } } diff --git a/OpenAI_API/Model/ModelsEndpoint.cs b/OpenAI_API/Model/ModelsEndpoint.cs index 9c260ce..66f34cc 100644 --- a/OpenAI_API/Model/ModelsEndpoint.cs +++ b/OpenAI_API/Model/ModelsEndpoint.cs @@ -13,27 +13,18 @@ namespace OpenAI_API /// /// The API endpoint for querying available models /// - public class ModelsEndpoint + public class ModelsEndpoint : EndpointBase { - OpenAIAPI Api; - /// - /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// The name of the enpoint, which is the final path segment in the API URL. For example, "models". /// - /// - internal ModelsEndpoint(OpenAIAPI api) - { - this.Api = api; - } + protected override string Endpoint { get { return "models"; } } /// - /// List all models via the API + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . /// - /// Asynchronously returns the list of all s - public Task> GetModelsAsync() - { - return GetModelsAsync(Api?.Auth); - } + /// + internal ModelsEndpoint(OpenAIAPI api) : base(api) { } /// /// Get details about a particular Model from the API, specifically properties such as and permissions. @@ -48,68 +39,29 @@ public Task RetrieveModelDetailsAsync(string id) /// /// List all models via the API /// - /// API authentication in order to call the API endpoint. If not specified, attempts to use a default. /// Asynchronously returns the list of all s - public static async Task> GetModelsAsync(APIAuthentication auth = null) + public async Task> GetModelsAsync() { - if (auth.ThisOrDefault()?.ApiKey is null) - { - throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); - } - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", auth.ThisOrDefault().ApiKey); - client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); - if (!string.IsNullOrEmpty(auth.ThisOrDefault().OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", auth.ThisOrDefault().OpenAIOrganization); - - var response = await client.GetAsync(@"https://api.openai.com/v1/models"); - string resultAsString = await response.Content.ReadAsStringAsync(); - - if (response.IsSuccessStatusCode) - { - var models = JsonConvert.DeserializeObject(resultAsString).data; - return models; - } - else - { - throw new HttpRequestException("Error calling OpenAi API to get list of models. HTTP status code: " + response.StatusCode.ToString() + ". Content: " + resultAsString); - } + return (await HttpGet()).data; } - /// - /// Get details about a particular Model from the API, specifically properties such as and permissions. - /// - /// The id/name of the model to get more details about - /// API authentication in order to call the API endpoint. If not specified, attempts to use a default. - /// Asynchronously returns the with all available properties - public static async Task RetrieveModelDetailsAsync(string id, APIAuthentication auth = null) + /// + /// Get details about a particular Model from the API, specifically properties such as and permissions. + /// + /// The id/name of the model to get more details about + /// API authentication in order to call the API endpoint. If not specified, attempts to use a default. + /// Asynchronously returns the with all available properties + public async Task RetrieveModelDetailsAsync(string id, APIAuthentication auth = null) { - if (auth.ThisOrDefault()?.ApiKey is null) - { - throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); - } - - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", auth.ThisOrDefault().ApiKey); - client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); - if (!string.IsNullOrEmpty(auth.ThisOrDefault().OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", auth.ThisOrDefault().OpenAIOrganization); - - var response = await client.GetAsync(@"https://api.openai.com/v1/models/" + id); - if (response.IsSuccessStatusCode) - { - string resultAsString = await response.Content.ReadAsStringAsync(); - var model = JsonConvert.DeserializeObject(resultAsString); - return model; - } - else - { - throw new HttpRequestException("Error calling OpenAi API to get model details. HTTP status code: " + response.StatusCode.ToString()); - } + string resultAsString = await HttpGetContent($"{Url}/{id}"); + var model = JsonConvert.DeserializeObject(resultAsString); + return model; } /// /// A helper class to deserialize the JSON API responses. This should not be used directly. /// - private class JsonHelperRoot + private class JsonHelperRoot : ApiResultBase { [JsonProperty("data")] public List data { get; set; } diff --git a/OpenAI_API/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index 2e307a6..b465805 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -18,7 +18,7 @@ public class OpenAIAPI /// /// Base url for OpenAI /// - public const string API_URL = "https://api.openai.com/v1/"; + public string ApiUrlBase = "https://api.openai.com/v1/"; /// /// The API authentication information to use for API calls @@ -34,7 +34,7 @@ public OpenAIAPI(APIAuthentication apiKeys = null) this.Auth = apiKeys.ThisOrDefault(); Completions = new CompletionEndpoint(this); Models = new ModelsEndpoint(this); - Search = new SearchEndpoint(this); + //Search = new SearchEndpoint(this); Files = new FilesEndpoint(this); } @@ -43,16 +43,16 @@ public OpenAIAPI(APIAuthentication apiKeys = null) /// public CompletionEndpoint Completions { get; } - /// - /// The API endpoint for querying available Engines/models - /// - public ModelsEndpoint Models { get; } + /// + /// The API endpoint for querying available Engines/models + /// + public ModelsEndpoint Models { get; } - /// - /// The API lets you do semantic search over documents. This means that you can provide a query, such as a natural language question or a statement, and find documents that answer the question or are semantically related to the statement. The “documents” can be words, sentences, paragraphs or even longer documents. For example, if you provide documents "White House", "hospital", "school" and query "the president", you’ll get a different similarity score for each document. The higher the similarity score, the more semantically similar the document is to the query (in this example, “White House” will be most similar to “the president”). - /// - [Obsolete("OpenAI no longer supports the Search endpoint")] - public SearchEndpoint Search { get; } + /// + /// The API lets you do semantic search over documents. This means that you can provide a query, such as a natural language question or a statement, and find documents that answer the question or are semantically related to the statement. The “documents” can be words, sentences, paragraphs or even longer documents. For example, if you provide documents "White House", "hospital", "school" and query "the president", you’ll get a different similarity score for each document. The higher the similarity score, the more semantically similar the document is to the query (in this example, “White House” will be most similar to “the president”). + /// + [Obsolete("OpenAI no longer supports the Search endpoint")] + public SearchEndpoint Search { get; } /// /// The API lets you do operations with files. You can upload, delete or retrieve files. Files can be used for fine-tuning, search, etc. diff --git a/OpenAI_API/Search/SearchEndpoint.cs b/OpenAI_API/Search/SearchEndpoint.cs index 1514a57..9544d84 100644 --- a/OpenAI_API/Search/SearchEndpoint.cs +++ b/OpenAI_API/Search/SearchEndpoint.cs @@ -9,12 +9,12 @@ namespace OpenAI_API { - // TODO: Maybe implement a shim based on https://github.com/openai/openai-cookbook/blob/main/transition_guides_for_deprecated_API_endpoints/search_functionality_example.py ? + // TODO: Maybe implement a shim based on https://github.com/openai/openai-cookbook/blob/main/transition_guides_for_deprecated_API_endpoints/search_functionality_example.py ? - /// - /// The API lets you do semantic search over documents. This means that you can provide a query, such as a natural language question or a statement, and find documents that answer the question or are semantically related to the statement. The “documents” can be words, sentences, paragraphs or even longer documents. For example, if you provide documents "White House", "hospital", "school" and query "the president", you’ll get a different similarity score for each document. The higher the similarity score, the more semantically similar the document is to the query (in this example, “White House” will be most similar to “the president”). - /// - [Obsolete("OpenAI no longer supports the Search endpoint")] + /// + /// The API lets you do semantic search over documents. This means that you can provide a query, such as a natural language question or a statement, and find documents that answer the question or are semantically related to the statement. The “documents” can be words, sentences, paragraphs or even longer documents. For example, if you provide documents "White House", "hospital", "school" and query "the president", you’ll get a different similarity score for each document. The higher the similarity score, the more semantically similar the document is to the query (in this example, “White House” will be most similar to “the president”). + /// + [Obsolete("OpenAI no longer supports the Search endpoint")] public class SearchEndpoint { OpenAIAPI Api; @@ -30,14 +30,14 @@ internal SearchEndpoint(OpenAIAPI api) - #region GetSearchResults - /// - /// Perform a semantic search over a list of documents - /// - /// The request containing the query and the documents to match against - /// Asynchronously returns a Dictionary mapping each document to the score for that document. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public async Task> GetSearchResultsAsync(SearchRequest request) + #region GetSearchResults + /// + /// Perform a semantic search over a list of documents + /// + /// The request containing the query and the documents to match against + /// Asynchronously returns a Dictionary mapping each document to the score for that document. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public async Task> GetSearchResultsAsync(SearchRequest request) { if (Api.Auth?.ApiKey is null) { @@ -47,7 +47,7 @@ public async Task> GetSearchResultsAsync(SearchReques HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Api.Auth.ApiKey); client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); - if (!string.IsNullOrEmpty(Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", Api.Auth.OpenAIOrganization); + if (!string.IsNullOrEmpty(Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", Api.Auth.OpenAIOrganization); string jsonContent = JsonConvert.SerializeObject(request, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore }); var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); @@ -77,43 +77,43 @@ public async Task> GetSearchResultsAsync(SearchReques } } - /// - /// Perform a semantic search over a list of documents, with a specific query - /// - /// The request containing the documents to match against - /// A query to search for, overriding whatever was provided in - /// Asynchronously returns a Dictionary mapping each document to the score for that document. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public Task> GetSearchResultsAsync(SearchRequest request, string query) + /// + /// Perform a semantic search over a list of documents, with a specific query + /// + /// The request containing the documents to match against + /// A query to search for, overriding whatever was provided in + /// Asynchronously returns a Dictionary mapping each document to the score for that document. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public Task> GetSearchResultsAsync(SearchRequest request, string query) { request.Query = query; return GetSearchResultsAsync(request); } - /// - /// Perform a semantic search of a query over a list of documents - /// - /// A query to match against - /// Documents to search over, provided as a list of strings - /// Asynchronously returns a Dictionary mapping each document to the score for that document. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public Task> GetSearchResultsAsync(string query, params string[] documents) + /// + /// Perform a semantic search of a query over a list of documents + /// + /// A query to match against + /// Documents to search over, provided as a list of strings + /// Asynchronously returns a Dictionary mapping each document to the score for that document. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public Task> GetSearchResultsAsync(string query, params string[] documents) { SearchRequest request = new SearchRequest(query, documents); return GetSearchResultsAsync(request); } - #endregion + #endregion - #region GetBestMatch + #region GetBestMatch - /// - /// Perform a semantic search over a list of documents to get the single best match - /// - /// The request containing the query and the documents to match against - /// Asynchronously returns the best matching document - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public async Task GetBestMatchAsync(SearchRequest request) + /// + /// Perform a semantic search over a list of documents to get the single best match + /// + /// The request containing the query and the documents to match against + /// Asynchronously returns the best matching document + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public async Task GetBestMatchAsync(SearchRequest request) { var results = await GetSearchResultsAsync(request); if (results.Count == 0) @@ -122,70 +122,70 @@ public async Task GetBestMatchAsync(SearchRequest request) return results.ToList().OrderByDescending(kv => kv.Value).FirstOrDefault().Key; } - /// - /// Perform a semantic search over a list of documents with a specific query to get the single best match - /// - /// The request containing the documents to match against - /// A query to search for, overriding whatever was provided in - /// Asynchronously returns the best matching document - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public Task GetBestMatchAsync(SearchRequest request, string query) + /// + /// Perform a semantic search over a list of documents with a specific query to get the single best match + /// + /// The request containing the documents to match against + /// A query to search for, overriding whatever was provided in + /// Asynchronously returns the best matching document + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public Task GetBestMatchAsync(SearchRequest request, string query) { request.Query = query; return GetBestMatchAsync(request); } - /// - /// Perform a semantic search of a query over a list of documents to get the single best match - /// - /// A query to match against - /// Documents to search over, provided as a list of strings - /// Asynchronously returns the best matching document - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public Task GetBestMatchAsync(string query, params string[] documents) + /// + /// Perform a semantic search of a query over a list of documents to get the single best match + /// + /// A query to match against + /// Documents to search over, provided as a list of strings + /// Asynchronously returns the best matching document + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public Task GetBestMatchAsync(string query, params string[] documents) { SearchRequest request = new SearchRequest(query, documents); return GetBestMatchAsync(request); } - #endregion + #endregion - #region GetBestMatchWithScore + #region GetBestMatchWithScore - /// - /// Perform a semantic search over a list of documents to get the single best match and its score - /// - /// The request containing the query and the documents to match against - /// Asynchronously returns a tuple of the best matching document and its score. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public async Task> GetBestMatchWithScoreAsync(SearchRequest request) + /// + /// Perform a semantic search over a list of documents to get the single best match and its score + /// + /// The request containing the query and the documents to match against + /// Asynchronously returns a tuple of the best matching document and its score. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public async Task> GetBestMatchWithScoreAsync(SearchRequest request) { var results = await GetSearchResultsAsync(request); var best = results.ToList().OrderByDescending(kv => kv.Value).FirstOrDefault(); return new Tuple(best.Key, best.Value); } - /// - /// Perform a semantic search over a list of documents with a specific query to get the single best match and its score - /// - /// The request containing the documents to match against - /// A query to search for, overriding whatever was provided in - /// Asynchronously returns a tuple of the best matching document and its score. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public Task> GetBestMatchWithScoreAsync(SearchRequest request, string query) + /// + /// Perform a semantic search over a list of documents with a specific query to get the single best match and its score + /// + /// The request containing the documents to match against + /// A query to search for, overriding whatever was provided in + /// Asynchronously returns a tuple of the best matching document and its score. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public Task> GetBestMatchWithScoreAsync(SearchRequest request, string query) { request.Query = query; return GetBestMatchWithScoreAsync(request); } - /// - /// Perform a semantic search of a query over a list of documents to get the single best match and its score - /// - /// A query to match against - /// Documents to search over, provided as a list of strings - /// Asynchronously returns a tuple of the best matching document and its score. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. - [Obsolete("OpenAI no long supports the Search endpoint", true)] - public Task> GetBestMatchWithScoreAsync(string query, params string[] documents) + /// + /// Perform a semantic search of a query over a list of documents to get the single best match and its score + /// + /// A query to match against + /// Documents to search over, provided as a list of strings + /// Asynchronously returns a tuple of the best matching document and its score. The similarity score is a positive score that usually ranges from 0 to 300 (but can sometimes go higher), where a score above 200 usually means the document is semantically similar to the query. + [Obsolete("OpenAI no long supports the Search endpoint", true)] + public Task> GetBestMatchWithScoreAsync(string query, params string[] documents) { SearchRequest request = new SearchRequest(query, documents); return GetBestMatchWithScoreAsync(request); diff --git a/OpenAI_API/Search/SearchRequest.cs b/OpenAI_API/Search/SearchRequest.cs index b4bbb4f..f968b74 100644 --- a/OpenAI_API/Search/SearchRequest.cs +++ b/OpenAI_API/Search/SearchRequest.cs @@ -6,8 +6,8 @@ namespace OpenAI_API { - [Obsolete("OpenAI no long supports the Search endpoint")] - public class SearchRequest + [Obsolete("OpenAI no long supports the Search endpoint")] + public class SearchRequest { [JsonProperty("documents")] @@ -16,13 +16,13 @@ public class SearchRequest [JsonProperty("query")] public string Query { get; set; } - /// - /// ID of the model to use. You can use to see all of your available models, or use a standard model like . Defaults to . - /// - [JsonProperty("model")] - public string Model { get; set; } = OpenAI_API.Model.DavinciCode; + /// + /// ID of the model to use. You can use to see all of your available models, or use a standard model like . Defaults to . + /// + [JsonProperty("model")] + public string Model { get; set; } = OpenAI_API.Model.DavinciCode; - public SearchRequest(string query = null, params string[] documents) + public SearchRequest(string query = null, params string[] documents) { Query = query; Documents = documents?.ToList() ?? new List(); diff --git a/OpenAI_API/Search/SearchResponse.cs b/OpenAI_API/Search/SearchResponse.cs index e1f625a..c49dbfd 100644 --- a/OpenAI_API/Search/SearchResponse.cs +++ b/OpenAI_API/Search/SearchResponse.cs @@ -5,11 +5,11 @@ namespace OpenAI_API { - /// - /// Used internally to deserialize a result from the Document Search API - /// - [Obsolete("OpenAI no long supports the Search endpoint")] - public class SearchResult + /// + /// Used internally to deserialize a result from the Document Search API + /// + [Obsolete("OpenAI no long supports the Search endpoint")] + public class SearchResult { /// /// The index of the document as originally supplied diff --git a/OpenAI_Tests/FilesEndpointTests.cs b/OpenAI_Tests/FilesEndpointTests.cs index c824a7b..a18074f 100644 --- a/OpenAI_Tests/FilesEndpointTests.cs +++ b/OpenAI_Tests/FilesEndpointTests.cs @@ -21,7 +21,7 @@ public void Setup() [Order(1)] public async Task UploadFile() { - var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var api = new OpenAI_API.OpenAIAPI(); var response = await api.Files.UploadFileAsync("fine-tuning-data.jsonl"); Assert.IsNotNull(response); Assert.IsTrue(response.Id.Length > 0); @@ -37,7 +37,7 @@ public async Task UploadFile() [Order(2)] public async Task ListFiles() { - var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var api = new OpenAI_API.OpenAIAPI(); var response = await api.Files.GetFilesAsync(); foreach (var file in response) @@ -52,7 +52,7 @@ public async Task ListFiles() [Order(3)] public async Task GetFile() { - var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var api = new OpenAI_API.OpenAIAPI(); var response = await api.Files.GetFilesAsync(); foreach (var file in response) { @@ -72,7 +72,7 @@ public async Task GetFile() [Order(4)] public async Task DeleteFiles() { - var api = new OpenAI_API.OpenAIAPI(engine: Engine.Davinci); + var api = new OpenAI_API.OpenAIAPI(); var response = await api.Files.GetFilesAsync(); foreach (var file in response) { From 82df6fb55186d3c374db4abd9a78939d6b0ad039 Mon Sep 17 00:00:00 2001 From: Roger Date: Thu, 2 Feb 2023 18:10:35 -0800 Subject: [PATCH 7/9] Additional tests from @Alexei000 and cleanup of usings (Thanks Alex Dragan!) --- OpenAI_API/APIAuthentication.cs | 5 - OpenAI_API/Completions/CompletionEndpoint.cs | 8 +- OpenAI_API/Completions/CompletionRequest.cs | 10 +- OpenAI_API/Completions/CompletionResult.cs | 6 +- OpenAI_API/EndpointBase.cs | 15 +- OpenAI_API/Files/File.cs | 5 - OpenAI_API/Files/FilesEndpoint.cs | 4 - OpenAI_API/Model/Model.cs | 8 +- OpenAI_API/Model/ModelsEndpoint.cs | 8 +- OpenAI_API/OpenAIAPI.cs | 9 +- OpenAI_API/Search/SearchRequest.cs | 1 - OpenAI_API/Search/SearchResponse.cs | 1 - OpenAI_Tests/AuthTests.cs | 30 +- OpenAI_Tests/CompletionEndpointTests.cs | 470 +++++++++++++++++-- OpenAI_Tests/FilesEndpointTests.cs | 4 - OpenAI_Tests/ModelEndpointTests.cs | 107 +++-- OpenAI_Tests/OpenAI_Tests.csproj | 7 +- OpenAI_Tests/SearchEndpointTests.cs | 3 - 18 files changed, 535 insertions(+), 166 deletions(-) diff --git a/OpenAI_API/APIAuthentication.cs b/OpenAI_API/APIAuthentication.cs index 709820a..1c1ff36 100644 --- a/OpenAI_API/APIAuthentication.cs +++ b/OpenAI_API/APIAuthentication.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Dynamic; using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; namespace OpenAI_API { diff --git a/OpenAI_API/Completions/CompletionEndpoint.cs b/OpenAI_API/Completions/CompletionEndpoint.cs index 42945dc..6b2b687 100644 --- a/OpenAI_API/Completions/CompletionEndpoint.cs +++ b/OpenAI_API/Completions/CompletionEndpoint.cs @@ -1,12 +1,6 @@ -using Newtonsoft.Json; -using System; +using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Security.Authentication; -using System.Text; using System.Threading.Tasks; namespace OpenAI_API diff --git a/OpenAI_API/Completions/CompletionRequest.cs b/OpenAI_API/Completions/CompletionRequest.cs index 423e677..7edcd4c 100644 --- a/OpenAI_API/Completions/CompletionRequest.cs +++ b/OpenAI_API/Completions/CompletionRequest.cs @@ -1,11 +1,5 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data.Common; using System.Linq; -using System.Runtime; -using System.Text; namespace OpenAI_API { @@ -18,7 +12,7 @@ public class CompletionRequest /// ID of the model to use. You can use to see all of your available models, or use a standard model like . /// [JsonProperty("model")] - public string Model { get; set; } + public string Model { get; set; } = OpenAI_API.Model.DavinciText; /// /// This is only used for serializing the request into JSON, do not use it directly. @@ -171,7 +165,7 @@ public string StopSequence /// public CompletionRequest() { - + this.Model = OpenAI_API.Model.DefaultModel; } /// diff --git a/OpenAI_API/Completions/CompletionResult.cs b/OpenAI_API/Completions/CompletionResult.cs index dadefba..98cf1bf 100644 --- a/OpenAI_API/Completions/CompletionResult.cs +++ b/OpenAI_API/Completions/CompletionResult.cs @@ -1,9 +1,5 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; using System.Collections.Generic; -using System.Text; -using System.Threading; namespace OpenAI_API { @@ -82,7 +78,7 @@ public class Logprobs public List Tokens { get; set; } [JsonProperty("token_logprobs")] - public List TokenLogprobs { get; set; } + public List TokenLogprobs { get; set; } [JsonProperty("top_logprobs")] public IList> TopLogprobs { get; set; } diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs index add1a1d..5362559 100644 --- a/OpenAI_API/EndpointBase.cs +++ b/OpenAI_API/EndpointBase.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Net.Http; -using System.Runtime.Serialization; using System.Security.Authentication; using System.Text; using System.Threading.Tasks; @@ -18,9 +17,9 @@ namespace OpenAI_API public abstract class EndpointBase { /// - /// + /// The internal reference to the API, mostly used for authentication /// - protected readonly OpenAIAPI Api; + protected readonly OpenAIAPI _Api; /// /// Constructor of the api endpoint base, to be called from the contructor of any devived classes. Rather than instantiating any endpoint yourself, access it through an instance of . @@ -28,7 +27,7 @@ public abstract class EndpointBase /// internal EndpointBase(OpenAIAPI api) { - this.Api = api; + this._Api = api; } /// @@ -43,7 +42,7 @@ protected string Url { get { - return $"{Api.ApiUrlBase}{Endpoint}"; + return $"{_Api.ApiUrlBase}{Endpoint}"; } } @@ -54,15 +53,15 @@ protected string Url /// Thrown if there is no valid authentication. Please refer to for details. protected HttpClient GetClient() { - if (Api.Auth?.ApiKey is null) + if (_Api.Auth?.ApiKey is null) { throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); } HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Api.Auth.ApiKey); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _Api.Auth.ApiKey); client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); - if (!string.IsNullOrEmpty(Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", Api.Auth.OpenAIOrganization); + if (!string.IsNullOrEmpty(_Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", _Api.Auth.OpenAIOrganization); return client; } diff --git a/OpenAI_API/Files/File.cs b/OpenAI_API/Files/File.cs index 1eb8c60..99bbc05 100644 --- a/OpenAI_API/Files/File.cs +++ b/OpenAI_API/Files/File.cs @@ -1,9 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OpenAI_API.Files { diff --git a/OpenAI_API/Files/FilesEndpoint.cs b/OpenAI_API/Files/FilesEndpoint.cs index 44c744e..9eb9dc0 100644 --- a/OpenAI_API/Files/FilesEndpoint.cs +++ b/OpenAI_API/Files/FilesEndpoint.cs @@ -1,11 +1,7 @@ using Newtonsoft.Json; -using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; -using System.Reflection; -using System.Text; using System.Threading.Tasks; namespace OpenAI_API.Files diff --git a/OpenAI_API/Model/Model.cs b/OpenAI_API/Model/Model.cs index e0ee2bd..bb38331 100644 --- a/OpenAI_API/Model/Model.cs +++ b/OpenAI_API/Model/Model.cs @@ -1,8 +1,4 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text; using System.Threading.Tasks; namespace OpenAI_API @@ -60,6 +56,10 @@ public Model() } + /// + /// The default model to use in requests if no other model is specified. + /// + public static Model DefaultModel { get; set; } = DavinciText; /// diff --git a/OpenAI_API/Model/ModelsEndpoint.cs b/OpenAI_API/Model/ModelsEndpoint.cs index 66f34cc..ad689af 100644 --- a/OpenAI_API/Model/ModelsEndpoint.cs +++ b/OpenAI_API/Model/ModelsEndpoint.cs @@ -1,11 +1,5 @@ using Newtonsoft.Json; -using System; using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Security.Authentication; -using System.Text; using System.Threading.Tasks; namespace OpenAI_API @@ -33,7 +27,7 @@ internal ModelsEndpoint(OpenAIAPI api) : base(api) { } /// Asynchronously returns the with all available properties public Task RetrieveModelDetailsAsync(string id) { - return RetrieveModelDetailsAsync(id, Api?.Auth); + return RetrieveModelDetailsAsync(id, _Api?.Auth); } /// diff --git a/OpenAI_API/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index b465805..e91b695 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -1,12 +1,5 @@ -using Newtonsoft.Json; -using OpenAI_API.Files; +using OpenAI_API.Files; using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; namespace OpenAI_API { diff --git a/OpenAI_API/Search/SearchRequest.cs b/OpenAI_API/Search/SearchRequest.cs index f968b74..ede89b0 100644 --- a/OpenAI_API/Search/SearchRequest.cs +++ b/OpenAI_API/Search/SearchRequest.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace OpenAI_API { diff --git a/OpenAI_API/Search/SearchResponse.cs b/OpenAI_API/Search/SearchResponse.cs index c49dbfd..ef30603 100644 --- a/OpenAI_API/Search/SearchResponse.cs +++ b/OpenAI_API/Search/SearchResponse.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Text; namespace OpenAI_API { diff --git a/OpenAI_Tests/AuthTests.cs b/OpenAI_Tests/AuthTests.cs index 11d837b..5d4b43f 100644 --- a/OpenAI_Tests/AuthTests.cs +++ b/OpenAI_Tests/AuthTests.cs @@ -9,12 +9,12 @@ public class AuthTests [SetUp] public void Setup() { - File.WriteAllText(".openai", "OPENAI_KEY=pk-test12"+Environment.NewLine+ "OPENAI_ORGANIZATION=org-testing123"); - Environment.SetEnvironmentVariable("OPENAI_API_KEY", "pk-test-env"); - Environment.SetEnvironmentVariable("OPENAI_ORGANIZATION", "org-testing123"); - } + File.WriteAllText(".openai", "OPENAI_KEY=pk-test12" + Environment.NewLine + "OPENAI_ORGANIZATION=org-testing123"); + Environment.SetEnvironmentVariable("OPENAI_API_KEY", "pk-test-env"); + Environment.SetEnvironmentVariable("OPENAI_ORGANIZATION", "org-testing123"); + } - [Test] + [Test] public void GetAuthFromEnv() { var auth = OpenAI_API.APIAuthentication.LoadFromEnv(); @@ -47,20 +47,20 @@ public void GetDefault() { var auth = OpenAI_API.APIAuthentication.Default; var envAuth = OpenAI_API.APIAuthentication.LoadFromEnv(); - Assert.IsNotNull(auth); - Assert.IsNotNull(auth.ApiKey); - Assert.IsNotNull(envAuth); - Assert.IsNotNull(envAuth.ApiKey); - Assert.AreEqual(envAuth.ApiKey, auth.ApiKey); - Assert.IsNotNull(auth.OpenAIOrganization); - Assert.IsNotNull(envAuth.OpenAIOrganization); - Assert.AreEqual(envAuth.OpenAIOrganization, auth.OpenAIOrganization); + Assert.IsNotNull(auth); + Assert.IsNotNull(auth.ApiKey); + Assert.IsNotNull(envAuth); + Assert.IsNotNull(envAuth.ApiKey); + Assert.AreEqual(envAuth.ApiKey, auth.ApiKey); + Assert.IsNotNull(auth.OpenAIOrganization); + Assert.IsNotNull(envAuth.OpenAIOrganization); + Assert.AreEqual(envAuth.OpenAIOrganization, auth.OpenAIOrganization); - } + } - [Test] + [Test] public void testHelper() { OpenAI_API.APIAuthentication defaultAuth = OpenAI_API.APIAuthentication.Default; diff --git a/OpenAI_Tests/CompletionEndpointTests.cs b/OpenAI_Tests/CompletionEndpointTests.cs index 74472c7..df5b93b 100644 --- a/OpenAI_Tests/CompletionEndpointTests.cs +++ b/OpenAI_Tests/CompletionEndpointTests.cs @@ -1,48 +1,438 @@ using NUnit.Framework; using OpenAI_API; using System; -using System.IO; using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using System.Collections.Generic; +using System.Net.Http; namespace OpenAI_Tests { - public class CompletionEndpointTests - { - [SetUp] - public void Setup() - { - OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); - } - - [Test] - public void GetBasicCompletion() - { - var api = new OpenAI_API.OpenAIAPI(); - - Assert.IsNotNull(api.Completions); - - var results = api.Completions.CreateCompletionsAsync(new CompletionRequest("One Two Three Four Five Six Seven Eight Nine One Two Three Four Five Six Seven Eight", model: Model.CurieText, temperature: 0.1, max_tokens: 5)).Result; - Assert.IsNotNull(results); - Assert.NotNull(results.Completions); - Assert.NotZero(results.Completions.Count); - Assert.That(results.Completions.Any(c => c.Text.Trim().ToLower().StartsWith("nine"))); - } - - - [Test] - public void GetSimpleCompletion() - { - var api = new OpenAI_API.OpenAIAPI(); - - Assert.IsNotNull(api.Completions); - - var results = api.Completions.CreateCompletionAsync("One Two Three Four Five Six Seven Eight Nine One Two Three Four Five Six Seven Eight", temperature: 0.1, max_tokens: 5).Result; - Assert.IsNotNull(results); - Assert.NotNull(results.Completions); - Assert.NotZero(results.Completions.Count); - Assert.That(results.Completions.Any(c => c.Text.Trim().ToLower().StartsWith("nine"))); - } - - // TODO: More tests needed but this covers basic functionality at least - } + public class CompletionEndpointTests + { + [SetUp] + public void Setup() + { + OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); + } + + [Test] + public void GetBasicCompletion() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Completions); + + var results = api.Completions.CreateCompletionsAsync(new CompletionRequest("One Two Three Four Five Six Seven Eight Nine One Two Three Four Five Six Seven Eight", model: Model.CurieText, temperature: 0.1, max_tokens: 5)).Result; + Assert.IsNotNull(results); + Assert.NotNull(results.Completions); + Assert.NotZero(results.Completions.Count); + Assert.That(results.Completions.Any(c => c.Text.Trim().ToLower().StartsWith("nine"))); + } + + + [Test] + public void GetSimpleCompletion() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Completions); + + var results = api.Completions.CreateCompletionAsync("One Two Three Four Five Six Seven Eight Nine One Two Three Four Five Six Seven Eight", temperature: 0.1, max_tokens: 5).Result; + Assert.IsNotNull(results); + Assert.NotNull(results.Completions); + Assert.NotZero(results.Completions.Count); + Assert.That(results.Completions.Any(c => c.Text.Trim().ToLower().StartsWith("nine"))); + } + + + [Test] + public async Task CreateCompletionAsync_MultiplePrompts_ShouldReturnResult() + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + MultiplePrompts = new[] + { + "Today is Monday, tomorrow is", + "10 11 12 13 14" + }, + Temperature = 0, + MaxTokens = 3 + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.ShouldContainAStringStartingWith("tuesday", "completion should contain next day"); + results.ShouldContainAStringStartingWith("15", "completion should contain next number"); + } + + + [TestCase(-0.2)] + [TestCase(3)] + public void CreateCompletionAsync_ShouldNotAllowTemperatureOutside01(double temperature) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = temperature, + MaxTokens = 10 + }; + + Func act = () => api.Completions.CreateCompletionsAsync(completionReq, 1); + act.Should() + .ThrowAsync() + .Where(exc => exc.Message.Contains("temperature")); + } + + [TestCase(1.8)] + [TestCase(1.9)] + [TestCase(2.0)] + public async Task ShouldBeMoreCreativeWithHighTemperature(double temperature) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = temperature, + MaxTokens = 5 + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Count.Should().Be(5, "completion count should be the default"); + results.Completions.Distinct().Count().Should().Be(results.Completions.Count); + } + + [TestCase(0.05)] + [TestCase(0.1)] + public async Task CreateCompletionAsync_ShouldGetSomeResultsWithVariousTopPValues(double topP) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = 0, + MaxTokens = 5, + TopP = topP + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Count.Should().Be(5, "completion count should be the default"); + } + + [TestCase(-0.5)] + [TestCase(0.0)] + [TestCase(0.5)] + [TestCase(1.0)] + public async Task CreateCompletionAsync_ShouldReturnSomeResultsForPresencePenalty(double presencePenalty) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = 0, + MaxTokens = 5, + PresencePenalty = presencePenalty + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Count.Should().Be(5, "completion count should be the default"); + } + + [TestCase(-0.5)] + [TestCase(0.0)] + [TestCase(0.5)] + [TestCase(1.0)] + public async Task CreateCompletionAsync_ShouldReturnSomeResultsForFrequencyPenalty(double frequencyPenalty) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = 0, + MaxTokens = 5, + FrequencyPenalty = frequencyPenalty + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Count.Should().Be(5, "completion count should be the default"); + } + + [Test] + public async Task CreateCompletionAsync_ShouldWorkForBiggerNumberOfCompletions() + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = 0, + MaxTokens = 5, + NumChoicesPerPrompt = 2 + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Count.Should().Be(5, "completion count should be the default"); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(5)] + public async Task CreateCompletionAsync_ShouldAlsoReturnLogProps(int logProps) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = 0, + MaxTokens = 5, + Logprobs = logProps + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Count.Should().Be(5, "completion count should be the default"); + results.Completions[0].Logprobs.TopLogprobs.Count.Should() + .Be(5, "logprobs should be returned for each completion"); + results.Completions[0].Logprobs.TopLogprobs[0].Keys.Count.Should().Be(logProps, + "because logprops count should be the same as requested"); + } + + [Test] + public async Task CreateCompletionAsync_Echo_ShouldReturnTheInput() + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "three four five", + Temperature = 0, + MaxTokens = 5, + Echo = true + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Should().OnlyContain(c => c.Text.StartsWith(completionReq.Prompt), "Echo should get the prompt back"); + } + + [TestCase("Thursday")] + [TestCase("Friday")] + public async Task CreateCompletionAsync_ShouldStopOnStopSequence(string stopSeq) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionReq = new CompletionRequest + { + Prompt = "Monday Tuesday Wednesday", + Temperature = 0, + MaxTokens = 5, + Echo = true, + StopSequence = stopSeq + }; + + var results = await api.Completions.CreateCompletionsAsync(completionReq); + results.ShouldNotBeEmpty(); + results.Completions.Should().OnlyContain(c => !c.Text.Contains(stopSeq), "Stop sequence must not be returned"); + results.Completions.Should().OnlyContain(c => c.FinishReason == "stop", "must end due to stop sequence"); + } + + [Test] + public async Task CreateCompletionAsync_MultipleParamShouldReturnTheSameDataAsSingleParamVersion() + { + var api = new OpenAI_API.OpenAIAPI(); + + var r = new CompletionRequest + { + Prompt = "three four five", + MaxTokens = 5, + Temperature = 0, + TopP = 0.1, + PresencePenalty = 0.5, + FrequencyPenalty = 0.3, + NumChoicesPerPrompt = 2, + Echo = true + }; + + var resultOneParam = await api.Completions.CreateCompletionsAsync(r); + resultOneParam.ShouldNotBeEmpty(); + + var resultsMultipleParams = await api.Completions.CreateCompletionAsync( + r.Prompt, Model.DefaultModel, r.MaxTokens, r.Temperature, r.TopP, r.NumChoicesPerPrompt, r.PresencePenalty, + r.FrequencyPenalty, + null, r.Echo); + resultsMultipleParams.ShouldNotBeEmpty(); + + resultOneParam.Should().BeEquivalentTo(resultsMultipleParams, opt => opt + .Excluding(o => o.Id) + .Excluding(o => o.CreatedUnixTime) + .Excluding(o => o.Created) + .Excluding(o => o.ProcessingTime) + .Excluding(o => o.RequestId) + ); + } + + [TestCase(5, 3)] + [TestCase(7, 2)] + public async Task StreamCompletionAsync_ShouldStreamIndexAndData(int maxTokens, int numOutputs) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionRequest = new CompletionRequest + { + Prompt = "three four five", + MaxTokens = maxTokens, + NumChoicesPerPrompt = numOutputs, + Temperature = 0, + TopP = 0.1, + PresencePenalty = 0.5, + FrequencyPenalty = 0.3, + Logprobs = 3, + Echo = true, + }; + + var streamIndexes = new List(); + var completionResults = new List(); + await api.Completions.StreamCompletionAsync(completionRequest, (index, result) => + { + streamIndexes.Add(index); + completionResults.Add(result); + }); + + int expectedCount = maxTokens * numOutputs; + streamIndexes.Count.Should().Be(expectedCount); + completionResults.Count.Should().Be(expectedCount); + } + + [TestCase(5, 3)] + [TestCase(7, 2)] + public async Task StreamCompletionAsync_ShouldStreamData(int maxTokens, int numOutputs) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionRequest = new CompletionRequest + { + Prompt = "three four five", + MaxTokens = maxTokens, + NumChoicesPerPrompt = numOutputs, + Temperature = 0, + TopP = 0.1, + PresencePenalty = 0.5, + FrequencyPenalty = 0.3, + Logprobs = 3, + Echo = true, + }; + + var completionResults = new List(); + await api.Completions.StreamCompletionAsync(completionRequest, result => + { + completionResults.Add(result); + }); + + int expectedCount = maxTokens * numOutputs; + completionResults.Count.Should().Be(expectedCount); + } + + [TestCase(5, 3)] + [TestCase(7, 2)] + public async Task StreamCompletionEnumerableAsync_ShouldStreamData(int maxTokens, int numOutputs) + { + var api = new OpenAI_API.OpenAIAPI(); + + var completionRequest = new CompletionRequest + { + Prompt = "three four five", + MaxTokens = maxTokens, + NumChoicesPerPrompt = numOutputs, + Temperature = 0, + TopP = 0.1, + PresencePenalty = 0.5, + FrequencyPenalty = 0.3, + Logprobs = 3, + Echo = true, + }; + + var completionResults = new List(); + await foreach (var res in api.Completions.StreamCompletionEnumerableAsync(completionRequest)) + { + completionResults.Add(res); + } + + int expectedCount = maxTokens * numOutputs; + completionResults.Count.Should().Be(expectedCount); + } + + [Test] + public async Task StreamCompletionEnumerableAsync_MultipleParamShouldReturnTheSameDataAsSingleParamVersion() + { + var api = new OpenAI_API.OpenAIAPI(); + + var r = new CompletionRequest + { + Prompt = "three four five", + MaxTokens = 5, + Temperature = 0, + TopP = 0.1, + PresencePenalty = 0.5, + FrequencyPenalty = 0.3, + NumChoicesPerPrompt = 2, + Logprobs = null, + Echo = true + }; + + var resultsOneParam = new List(); + await foreach (var res in api.Completions.StreamCompletionEnumerableAsync(r)) + { + resultsOneParam.Add(res); + } + + resultsOneParam.Should().NotBeEmpty("At least one result should be fetched"); + + var resultsMultipleParams = new List(); + await foreach (var res in api.Completions.StreamCompletionEnumerableAsync( + r.Prompt, Model.DefaultModel, r.MaxTokens, r.Temperature, r.TopP, r.NumChoicesPerPrompt, r.PresencePenalty, + r.FrequencyPenalty, + null, r.Echo)) + { + resultsMultipleParams.Add(res); + } + resultsMultipleParams.Should().NotBeEmpty(); + + resultsOneParam.Should().BeEquivalentTo(resultsMultipleParams, opt => opt + .Excluding(o => o.Id) + .Excluding(o => o.CreatedUnixTime) + .Excluding(o => o.Created) + .Excluding(o => o.ProcessingTime) + .Excluding(o => o.RequestId) + ); + } + } + + public static class CompletionTestingHelper + { public static void ShouldNotBeEmpty(this CompletionResult results) + { + results.Should().NotBeNull("a result must be received"); + results.Completions.Should().NotBeNull("completions must be received"); + results.Completions.Should().NotBeEmpty("completions must be non-empty"); + } + + public static void ShouldContainAStringStartingWith(this CompletionResult results, string startToken, string because = "") + { + results.Completions.Should().Contain(c => c.Text.Trim().ToLower().StartsWith(startToken), because); + } + } } \ No newline at end of file diff --git a/OpenAI_Tests/FilesEndpointTests.cs b/OpenAI_Tests/FilesEndpointTests.cs index a18074f..6cf3df8 100644 --- a/OpenAI_Tests/FilesEndpointTests.cs +++ b/OpenAI_Tests/FilesEndpointTests.cs @@ -1,9 +1,5 @@ using NUnit.Framework; -using OpenAI_API; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/OpenAI_Tests/ModelEndpointTests.cs b/OpenAI_Tests/ModelEndpointTests.cs index 8b9e92e..c710799 100644 --- a/OpenAI_Tests/ModelEndpointTests.cs +++ b/OpenAI_Tests/ModelEndpointTests.cs @@ -1,45 +1,76 @@ -using NUnit.Framework; +using FluentAssertions; +using NUnit.Framework; using OpenAI_API; using System; -using System.IO; using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; namespace OpenAI_Tests { - public class ModelEndpointTests - { - [SetUp] - public void Setup() - { - OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); - } - - [Test] - public void GetAllModels() - { - var api = new OpenAI_API.OpenAIAPI(); - - Assert.IsNotNull(api.Models); - - var results = api.Models.GetModelsAsync().Result; - Assert.IsNotNull(results); - Assert.NotZero(results.Count); - Assert.That(results.Any(c => c.ModelID.ToLower().StartsWith("text-davinci"))); - } - - [Test] - public void GetModelDetails() - { - var api = new OpenAI_API.OpenAIAPI(); - - Assert.IsNotNull(api.Models); - - var result = api.Models.RetrieveModelDetailsAsync(Model.DavinciText.ModelID).Result; - Assert.IsNotNull(result); - Assert.IsNotNull(result.ModelID); - Assert.IsNotNull(result.OwnedBy); - Assert.AreEqual(Model.DavinciText.ModelID.ToLower(), result.ModelID.ToLower()); - } - // TODO: More tests needed but this covers basic functionality at least - } + public class ModelEndpointTests + { + [SetUp] + public void Setup() + { + OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); + } + + [Test] + public void GetAllModels() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Models); + + var results = api.Models.GetModelsAsync().Result; + Assert.IsNotNull(results); + Assert.NotZero(results.Count); + Assert.That(results.Any(c => c.ModelID.ToLower().StartsWith("text-davinci"))); + } + + [Test] + public void GetModelDetails() + { + var api = new OpenAI_API.OpenAIAPI(); + + Assert.IsNotNull(api.Models); + + var result = api.Models.RetrieveModelDetailsAsync(Model.DavinciText.ModelID).Result; + Assert.IsNotNull(result); + Assert.IsNotNull(result.ModelID); + Assert.IsNotNull(result.OwnedBy); + Assert.AreEqual(Model.DavinciText.ModelID.ToLower(), result.ModelID.ToLower()); + } + + + [Test] + public async Task GetEnginesAsync_ShouldReturnTheEngineList() + { + var api = new OpenAI_API.OpenAIAPI(); + var models = await api.Models.GetModelsAsync(); + models.Count.Should().BeGreaterOrEqualTo(5, "most engines should be returned"); + } + + [Test] + public void GetEnginesAsync_ShouldFailIfInvalidAuthIsProvided() + { + var api = new OpenAIAPI(new APIAuthentication(Guid.NewGuid().ToString())); + Func act = () => api.Models.GetModelsAsync(); + act.Should() + .ThrowAsync() + .Where(exc => exc.Message.Contains("Incorrect API key provided")); + } + + [TestCase("ada")] + [TestCase("babbage")] + [TestCase("curie")] + [TestCase("davinci")] + public async Task RetrieveEngineDetailsAsync_ShouldRetrieveEngineDetails(string modelId) + { + var api = new OpenAI_API.OpenAIAPI(); + var modelData = await api.Models.RetrieveModelDetailsAsync(modelId); + modelData?.ModelID?.Should()?.Be(modelId); + } + } } \ No newline at end of file diff --git a/OpenAI_Tests/OpenAI_Tests.csproj b/OpenAI_Tests/OpenAI_Tests.csproj index 9da037d..805cc44 100644 --- a/OpenAI_Tests/OpenAI_Tests.csproj +++ b/OpenAI_Tests/OpenAI_Tests.csproj @@ -7,9 +7,10 @@ - - - + + + + diff --git a/OpenAI_Tests/SearchEndpointTests.cs b/OpenAI_Tests/SearchEndpointTests.cs index 8c8f05f..9ea6345 100644 --- a/OpenAI_Tests/SearchEndpointTests.cs +++ b/OpenAI_Tests/SearchEndpointTests.cs @@ -1,8 +1,5 @@ using NUnit.Framework; -using OpenAI_API; using System; -using System.IO; -using System.Linq; namespace OpenAI_Tests { From b49a2178976682003df121a8c9c6baf7ce9fcc0e Mon Sep 17 00:00:00 2001 From: Roger Date: Thu, 2 Feb 2023 18:19:23 -0800 Subject: [PATCH 8/9] Pulled the user agent out into a constant --- OpenAI_API/EndpointBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs index 5362559..b25336c 100644 --- a/OpenAI_API/EndpointBase.cs +++ b/OpenAI_API/EndpointBase.cs @@ -16,6 +16,8 @@ namespace OpenAI_API /// public abstract class EndpointBase { + private const string Value = "okgodoit/dotnet_openai_api"; + /// /// The internal reference to the API, mostly used for authentication /// @@ -60,7 +62,7 @@ protected HttpClient GetClient() HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _Api.Auth.ApiKey); - client.DefaultRequestHeaders.Add("User-Agent", "okgodoit/dotnet_openai_api"); + client.DefaultRequestHeaders.Add("User-Agent", Value); if (!string.IsNullOrEmpty(_Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", _Api.Auth.OpenAIOrganization); return client; From 607f52cd7281b68d9769ade0d9bf78adde41e678 Mon Sep 17 00:00:00 2001 From: Roger Date: Thu, 2 Feb 2023 18:44:28 -0800 Subject: [PATCH 9/9] Fix small date parsing bug in model details --- OpenAI_API/Model/Model.cs | 212 ++++++++++++------------ OpenAI_Tests/CompletionEndpointTests.cs | 3 +- OpenAI_Tests/ModelEndpointTests.cs | 2 + 3 files changed, 110 insertions(+), 107 deletions(-) diff --git a/OpenAI_API/Model/Model.cs b/OpenAI_API/Model/Model.cs index c1a9ee9..67a8f84 100644 --- a/OpenAI_API/Model/Model.cs +++ b/OpenAI_API/Model/Model.cs @@ -5,11 +5,11 @@ namespace OpenAI_API { - /// - /// Represents a language model - /// - public class Model - { + /// + /// Represents a language model + /// + public class Model + { /// /// The id/name of the model /// @@ -30,7 +30,7 @@ public class Model /// The time when the model was created [JsonIgnore] - public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime.Value).DateTime; + public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; /// /// The time when the model was created in unix epoch format @@ -62,35 +62,35 @@ public class Model /// The to cast to a string. public static implicit operator string(Model model) { - return model?.ModelID; + return model?.ModelID; } /// - /// Allows a string to be implicitly cast as an with that - /// - /// The id/ to use - public static implicit operator Model(string name) - { - return new Model(name); - } + /// Allows a string to be implicitly cast as an with that + /// + /// The id/ to use + public static implicit operator Model(string name) + { + return new Model(name); + } - /// - /// Represents an Model with the given id/ - /// - /// The id/ to use. - /// - public Model(string name) - { - this.ModelID = name; - } + /// + /// Represents an Model with the given id/ + /// + /// The id/ to use. + /// + public Model(string name) + { + this.ModelID = name; + } - /// - /// Represents a generic Model/model - /// - public Model() - { + /// + /// Represents a generic Model/model + /// + public Model() + { - } + } /// /// The default model to use in requests if no other model is specified. @@ -144,85 +144,85 @@ public async Task RetrieveModelDetailsAsync(OpenAI_API.OpenAIAPI api) return await api.Models.RetrieveModelDetailsAsync(this.ModelID); } - } - - /// - /// Permissions for using the model - /// - public class Permissions - { - /// - /// Permission Id (not to be confused with ModelId) - /// - [JsonProperty("id")] - public string Id { get; set; } - - /// - /// Object type, should always be 'model_permission' - /// - [JsonProperty("object")] - public string Object { get; set; } + } + + /// + /// Permissions for using the model + /// + public class Permissions + { + /// + /// Permission Id (not to be confused with ModelId) + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Object type, should always be 'model_permission' + /// + [JsonProperty("object")] + public string Object { get; set; } /// The time when the permission was created [JsonIgnore] - public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime.Value).DateTime; - - /// - /// Unix timestamp for creation date/time - /// - [JsonProperty("created")] - public long CreatedUnixTime { get; set; } - - /// - /// Can the model be created? - /// - [JsonProperty("allow_create_engine")] - public bool AllowCreateEngine { get; set; } - - /// - /// Does the model support temperature sampling? - /// https://beta.openai.com/docs/api-reference/completions/create#completions/create-temperature - /// - [JsonProperty("allow_sampling")] - public bool AllowSampling { get; set; } - - /// - /// Does the model support logprobs? - /// https://beta.openai.com/docs/api-reference/completions/create#completions/create-logprobs - /// - [JsonProperty("allow_logprobs")] - public bool AllowLogProbs { get; set; } - - /// - /// Does the model support search indices? - /// - [JsonProperty("allow_search_indices")] - public bool AllowSearchIndices { get; set; } - - [JsonProperty("allow_view")] - public bool AllowView { get; set; } - - /// - /// Does the model allow fine tuning? - /// https://beta.openai.com/docs/api-reference/fine-tunes - /// - [JsonProperty("allow_fine_tuning")] - public bool AllowFineTuning { get; set; } - - /// - /// Is the model only allowed for a particular organization? May not be implemented yet. - /// - [JsonProperty("organization")] - public string Organization { get; set; } - - /// - /// Is the model part of a group? Seems not implemented yet. Always null. - /// - [JsonProperty("group")] - public string Group { get; set; } - - [JsonProperty("is_blocking")] - public bool IsBlocking { get; set; } - } + public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + + /// + /// Unix timestamp for creation date/time + /// + [JsonProperty("created")] + public long CreatedUnixTime { get; set; } + + /// + /// Can the model be created? + /// + [JsonProperty("allow_create_engine")] + public bool AllowCreateEngine { get; set; } + + /// + /// Does the model support temperature sampling? + /// https://beta.openai.com/docs/api-reference/completions/create#completions/create-temperature + /// + [JsonProperty("allow_sampling")] + public bool AllowSampling { get; set; } + + /// + /// Does the model support logprobs? + /// https://beta.openai.com/docs/api-reference/completions/create#completions/create-logprobs + /// + [JsonProperty("allow_logprobs")] + public bool AllowLogProbs { get; set; } + + /// + /// Does the model support search indices? + /// + [JsonProperty("allow_search_indices")] + public bool AllowSearchIndices { get; set; } + + [JsonProperty("allow_view")] + public bool AllowView { get; set; } + + /// + /// Does the model allow fine tuning? + /// https://beta.openai.com/docs/api-reference/fine-tunes + /// + [JsonProperty("allow_fine_tuning")] + public bool AllowFineTuning { get; set; } + + /// + /// Is the model only allowed for a particular organization? May not be implemented yet. + /// + [JsonProperty("organization")] + public string Organization { get; set; } + + /// + /// Is the model part of a group? Seems not implemented yet. Always null. + /// + [JsonProperty("group")] + public string Group { get; set; } + + [JsonProperty("is_blocking")] + public bool IsBlocking { get; set; } + } } diff --git a/OpenAI_Tests/CompletionEndpointTests.cs b/OpenAI_Tests/CompletionEndpointTests.cs index df5b93b..a56d9a7 100644 --- a/OpenAI_Tests/CompletionEndpointTests.cs +++ b/OpenAI_Tests/CompletionEndpointTests.cs @@ -423,7 +423,8 @@ public async Task StreamCompletionEnumerableAsync_MultipleParamShouldReturnTheSa } public static class CompletionTestingHelper - { public static void ShouldNotBeEmpty(this CompletionResult results) + { + public static void ShouldNotBeEmpty(this CompletionResult results) { results.Should().NotBeNull("a result must be received"); results.Completions.Should().NotBeNull("completions must be received"); diff --git a/OpenAI_Tests/ModelEndpointTests.cs b/OpenAI_Tests/ModelEndpointTests.cs index c710799..53dba1c 100644 --- a/OpenAI_Tests/ModelEndpointTests.cs +++ b/OpenAI_Tests/ModelEndpointTests.cs @@ -71,6 +71,8 @@ public async Task RetrieveEngineDetailsAsync_ShouldRetrieveEngineDetails(string var api = new OpenAI_API.OpenAIAPI(); var modelData = await api.Models.RetrieveModelDetailsAsync(modelId); modelData?.ModelID?.Should()?.Be(modelId); + modelData.Created.Should().BeAfter(new DateTime(2018, 1, 1), "the model has a created date no earlier than 2018"); + modelData.Created.Should().BeBefore(DateTime.Now.AddDays(1), "the model has a created date before today"); } } } \ No newline at end of file