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/APIAuthentication.cs b/OpenAI_API/APIAuthentication.cs index d9fd4c0..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 { @@ -17,10 +12,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 +36,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 +74,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 +123,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/Completions/CompletionEndpoint.cs b/OpenAI_API/Completions/CompletionEndpoint.cs index 37803e3..6b2b687 100644 --- a/OpenAI_API/Completions/CompletionEndpoint.cs +++ b/OpenAI_API/Completions/CompletionEndpoint.cs @@ -1,354 +1,238 @@ -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 { - /// - /// 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..7edcd4c 100644 --- a/OpenAI_API/Completions/CompletionRequest.cs +++ b/OpenAI_API/Completions/CompletionRequest.cs @@ -1,254 +1,248 @@ 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 { - /// - /// 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; } = OpenAI_API.Model.DavinciText; + + /// + /// 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() + { + this.Model = OpenAI_API.Model.DefaultModel; + } + + /// + /// 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..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 { @@ -48,7 +44,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 +52,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 @@ -116,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 new file mode 100644 index 0000000..b25336c --- /dev/null +++ b/OpenAI_API/EndpointBase.cs @@ -0,0 +1,361 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +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 + { + private const string Value = "okgodoit/dotnet_openai_api"; + + /// + /// The internal reference to the API, mostly used for authentication + /// + 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", Value); + 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 new file mode 100644 index 0000000..99bbc05 --- /dev/null +++ b/OpenAI_API/Files/File.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; + +namespace OpenAI_API.Files +{ + /// + /// 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 + /// + [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; } + + /// + /// 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; } + + /// + /// 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") + /// + [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..9eb9dc0 --- /dev/null +++ b/OpenAI_API/Files/FilesEndpoint.cs @@ -0,0 +1,97 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace OpenAI_API.Files +{ + /// + /// The API endpoint for operations List, Upload, Delete, Retrieve files + /// + public class FilesEndpoint : EndpointBase + { + /// + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// + /// + internal FilesEndpoint(OpenAIAPI api) : base(api) { } + + /// + /// 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 + /// + /// + /// + public async Task> GetFilesAsync() + { + return (await HttpGet()).Data; + } + + /// + /// Returns information about a specific file + /// + /// The ID of the file to use for this request + /// + public async Task GetFileAsync(string fileId) + { + return await HttpGet($"{Url}/{fileId}"); + } + + + /// + /// 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) + { + return await HttpGetContent($"{Url}/{fileId}/content"); + } + + /// + /// Delete a file + /// + /// The ID of the file to use for this request + /// + public async Task DeleteFileAsync(string fileId) + { + 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 OpenAI 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 filePath, string purpose = "fine-tune") + { + HttpClient client = GetClient(); + var content = new MultipartFormDataContent + { + { new StringContent(purpose), "purpose" }, + { new ByteArrayContent(System.IO.File.ReadAllBytes(filePath)), "file", Path.GetFileName(filePath) } + }; + + 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; } + } + } + + +} diff --git a/OpenAI_API/Model/Model.cs b/OpenAI_API/Model/Model.cs index 1d321a4..67a8f84 100644 --- a/OpenAI_API/Model/Model.cs +++ b/OpenAI_API/Model/Model.cs @@ -1,8 +1,6 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Net.Http; -using System.Text; using System.Threading.Tasks; namespace OpenAI_API @@ -18,17 +16,51 @@ 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; } + + /// + /// The type of object. Should always be 'model'. + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// The time when the model was created + [JsonIgnore] + public DateTime Created => DateTimeOffset.FromUnixTimeSeconds(CreatedUnixTime).DateTime; + + /// + /// The time when the model was created in unix epoch format + /// + [JsonProperty("created")] + public long CreatedUnixTime { 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; } @@ -60,53 +92,137 @@ public Model() } + /// + /// The default model to use in requests if no other model is specified. + /// + public static Model DefaultModel { get; set; } = DavinciText; - /// - /// 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 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" }; - /// - /// Very capable, but faster and lower cost than Davinci. - /// - public static Model CurieText => new Model("text-curie-001") { OwnedBy = "openai" }; + /// + /// Capable of straightforward tasks, very fast, and lower cost. + /// + public static Model BabbageText => new Model("text-babbage-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" }; + /// + /// Very capable, but faster and lower cost than Davinci. + /// + public static Model CurieText => new Model("text-curie-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 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 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" }; + /// + /// 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" }; - /// - /// 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" }; + /// + /// 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" }; - /// - /// 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); + } + + } + + /// + /// 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).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_API/Model/ModelsEndpoint.cs b/OpenAI_API/Model/ModelsEndpoint.cs index 9c260ce..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 @@ -13,27 +7,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. @@ -42,74 +27,35 @@ public Task> GetModelsAsync() /// Asynchronously returns the with all available properties public Task RetrieveModelDetailsAsync(string id) { - return RetrieveModelDetailsAsync(id, Api?.Auth); + return RetrieveModelDetailsAsync(id, _Api?.Auth); } /// /// 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 3bc3ad8..d764408 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -1,62 +1,60 @@ -using Newtonsoft.Json; +using OpenAI_API.Files; +using Newtonsoft.Json; using OpenAI_API.Embedding; 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 { - /// - /// Entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints - /// - public class OpenAIAPI - { - /// - /// The API authentication information to use for API calls - /// - public APIAuthentication Auth { get; set; } - - /// - /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints - /// - /// The API authentication information to use for API calls, or to attempt to use the , potentially loading from environment vars or from a config file. - public OpenAIAPI(APIAuthentication apiKeys = null) - { - this.Auth = apiKeys.ThisOrDefault(); - Completions = new CompletionEndpoint(this); - Models = new ModelsEndpoint(this); - Search = new SearchEndpoint(this); - Embeddings = new EmbeddingEndpoint(this); - } - - /// - /// 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 CompletionEndpoint Completions { get; } - - /// - /// The API lets you transform text into a vector (list) of floating point numbers. The distance between two vectors measures their relatedness. Small distances suggest high relatedness and large distances suggest low relatedness. - /// - public EmbeddingEndpoint Embeddings { 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; } - - - - - - } + /// + /// Entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints + /// + public class OpenAIAPI + { + /// + /// The API authentication information to use for API calls + /// + public APIAuthentication Auth { get; set; } + + /// + /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints + /// + /// The API authentication information to use for API calls, or to attempt to use the , potentially loading from environment vars or from a config file. + public OpenAIAPI(APIAuthentication apiKeys = null) + { + this.Auth = apiKeys.ThisOrDefault(); + Completions = new CompletionEndpoint(this); + Models = new ModelsEndpoint(this); + Search = new SearchEndpoint(this); + Embeddings = new EmbeddingEndpoint(this); + } + + /// + /// 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 CompletionEndpoint Completions { get; } + + /// + /// The API lets you transform text into a vector (list) of floating point numbers. The distance between two vectors measures their relatedness. Small distances suggest high relatedness and large distances suggest low relatedness. + /// + public EmbeddingEndpoint Embeddings { 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 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_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 @@ - + 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..ede89b0 100644 --- a/OpenAI_API/Search/SearchRequest.cs +++ b/OpenAI_API/Search/SearchRequest.cs @@ -2,12 +2,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; 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 +15,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..ef30603 100644 --- a/OpenAI_API/Search/SearchResponse.cs +++ b/OpenAI_API/Search/SearchResponse.cs @@ -1,15 +1,14 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Text; 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/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..a56d9a7 100644 --- a/OpenAI_Tests/CompletionEndpointTests.cs +++ b/OpenAI_Tests/CompletionEndpointTests.cs @@ -1,48 +1,439 @@ 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 new file mode 100644 index 0000000..6cf3df8 --- /dev/null +++ b/OpenAI_Tests/FilesEndpointTests.cs @@ -0,0 +1,87 @@ +using NUnit.Framework; +using System; +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(); + 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(); + 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(); + 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(); + 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/ModelEndpointTests.cs b/OpenAI_Tests/ModelEndpointTests.cs index 8b9e92e..53dba1c 100644 --- a/OpenAI_Tests/ModelEndpointTests.cs +++ b/OpenAI_Tests/ModelEndpointTests.cs @@ -1,45 +1,78 @@ -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); + 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 diff --git a/OpenAI_Tests/OpenAI_Tests.csproj b/OpenAI_Tests/OpenAI_Tests.csproj index 56b6dea..805cc44 100644 --- a/OpenAI_Tests/OpenAI_Tests.csproj +++ b/OpenAI_Tests/OpenAI_Tests.csproj @@ -7,13 +7,20 @@ - - - + + + + + + + PreserveNewest + + + 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 { 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).###"}