From e6155b421df7f6028cc0d6579aebdb5886d5eacf Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Wed, 15 Feb 2023 09:14:00 +0000 Subject: [PATCH 1/9] fix: deprecate overload that ignores param closes #42 --- OpenAI_API/Model/ModelsEndpoint.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/OpenAI_API/Model/ModelsEndpoint.cs b/OpenAI_API/Model/ModelsEndpoint.cs index a197e68..16cc147 100644 --- a/OpenAI_API/Model/ModelsEndpoint.cs +++ b/OpenAI_API/Model/ModelsEndpoint.cs @@ -25,10 +25,12 @@ internal ModelsEndpoint(OpenAIAPI api) : base(api) { } /// /// The id/name of the model to get more details about /// Asynchronously returns the with all available properties - public Task RetrieveModelDetailsAsync(string id) + public async Task RetrieveModelDetailsAsync(string id) { - return RetrieveModelDetailsAsync(id, _Api?.Auth); - } + string resultAsString = await HttpGetContent($"{Url}/{id}"); + var model = JsonConvert.DeserializeObject(resultAsString); + return model; + } /// /// List all models via the API @@ -43,14 +45,11 @@ public async Task> GetModelsAsync() /// 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. + /// Obsolete: IGNORED /// Asynchronously returns the with all available properties - public async Task RetrieveModelDetailsAsync(string id, APIAuthentication auth = null) - { - string resultAsString = await HttpGetContent($"{Url}/{id}"); - var model = JsonConvert.DeserializeObject(resultAsString); - return model; - } + [Obsolete("Use the overload without the APIAuthentication parameter instead, as it is ignored.", false)] // See #42 + public Task RetrieveModelDetailsAsync(string id, APIAuthentication auth) => + this.RetrieveModelDetailsAsync(id); /// /// A helper class to deserialize the JSON API responses. This should not be used directly. From 945dd2a03869840363395a22ced2442cf372d80d Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Wed, 15 Feb 2023 09:16:50 +0000 Subject: [PATCH 2/9] fix: removed unused new HttpClient() closes #43 --- OpenAI_API/Files/FilesEndpoint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenAI_API/Files/FilesEndpoint.cs b/OpenAI_API/Files/FilesEndpoint.cs index 6721a2e..b58eb50 100644 --- a/OpenAI_API/Files/FilesEndpoint.cs +++ b/OpenAI_API/Files/FilesEndpoint.cs @@ -71,7 +71,6 @@ public async Task DeleteFileAsync(string fileId) /// 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" }, From 71f30ccf616a8ac43de6942c2b90b20b25268577 Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Wed, 15 Feb 2023 10:25:42 +0000 Subject: [PATCH 3/9] feat: support dependency injection this is a breaking change, it is not backwards compatible closes #41 --- OpenAI_API/APIAuthentication.cs | 28 ---------------------- OpenAI_API/EndpointBase.cs | 17 +++++-------- OpenAI_API/IOpenAI.cs | 23 ++++++++++++++++++ OpenAI_API/OpenAIAPI.cs | 40 +++++++++++++++---------------- OpenAI_API/OpenAIApiExtensions.cs | 31 ++++++++++++++++++++++++ OpenAI_API/OpenAI_API.csproj | 11 +++++---- 6 files changed, 86 insertions(+), 64 deletions(-) create mode 100644 OpenAI_API/IOpenAI.cs create mode 100644 OpenAI_API/OpenAIApiExtensions.cs diff --git a/OpenAI_API/APIAuthentication.cs b/OpenAI_API/APIAuthentication.cs index 4346281..7a739c1 100644 --- a/OpenAI_API/APIAuthentication.cs +++ b/OpenAI_API/APIAuthentication.cs @@ -157,34 +157,6 @@ public static APIAuthentication LoadFromPath(string directory = null, string fil return new APIAuthentication(key, org); } - - - /// - /// Tests the api key against the OpenAI API, to ensure it is valid. This hits the models endpoint so should not be charged for usage. - /// - /// if the api key is valid, or if empty or not accepted by the OpenAI API. - public async Task ValidateAPIKey() - { - if (string.IsNullOrEmpty(ApiKey)) - return false; - - var api = new OpenAIAPI(this); - - List results; - - try - { - results = await api.Models.GetModelsAsync(); - } - catch (Exception ex) - { - Debug.WriteLine(ex.ToString()); - return false; - } - - return (results.Count > 0); - } - } internal static class AuthHelpers diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs index 5d74cfa..a8fe1c0 100644 --- a/OpenAI_API/EndpointBase.cs +++ b/OpenAI_API/EndpointBase.cs @@ -48,22 +48,19 @@ protected string Url } } - /// - /// Gets an HTTPClient with the appropriate authorization and other headers set - /// + /// Configures 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() + internal static HttpClient ConfigureClient(HttpClient client, APIAuthentication auth) { - if (_Api.Auth?.ApiKey is null) + if (auth?.ApiKey is null) { throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); } - HttpClient client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _Api.Auth.ApiKey); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", auth.ApiKey); client.DefaultRequestHeaders.Add("User-Agent", Value); - if (!string.IsNullOrEmpty(_Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", _Api.Auth.OpenAIOrganization); + if (!string.IsNullOrEmpty(auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", auth.OpenAIOrganization); return client; } @@ -99,8 +96,6 @@ private async Task HttpRequestRaw(string url = null, HttpMe if (verb == null) verb = HttpMethod.Get; - var client = GetClient(); - HttpResponseMessage response = null; string resultAsString = null; HttpRequestMessage req = new HttpRequestMessage(verb, url); @@ -118,7 +113,7 @@ private async Task HttpRequestRaw(string url = null, HttpMe req.Content = stringContent; } } - response = await client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + response = await this._Api.Client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); if (response.IsSuccessStatusCode) { diff --git a/OpenAI_API/IOpenAI.cs b/OpenAI_API/IOpenAI.cs new file mode 100644 index 0000000..37d027f --- /dev/null +++ b/OpenAI_API/IOpenAI.cs @@ -0,0 +1,23 @@ +#nullable enable +namespace OpenAI_API; +using OpenAI_API.Completions; +using OpenAI_API.Embedding; +using OpenAI_API.Files; +using OpenAI_API.Models; + +/// Entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints +public interface IOpenAI +{ + /// 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 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/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index 727b261..de7c164 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -1,36 +1,37 @@ -using OpenAI_API.Completions; +using Microsoft.Extensions.Logging; +using OpenAI_API.Completions; using OpenAI_API.Embedding; using OpenAI_API.Files; using OpenAI_API.Models; +using System.Net.Http; namespace OpenAI_API { /// /// Entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints /// - public class OpenAIAPI - { + public class OpenAIAPI: IOpenAI + { /// /// Base url for OpenAI /// public string ApiUrlBase = "https://api.openai.com/v1/"; - - /// - /// 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) + /// The HTTP client configured with the authentication headers. + internal HttpClient Client { get; } + + /// + /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints + /// + /// The HTTP client configured with the authentication headers. + public OpenAIAPI( + HttpClient httpClient) { - this.Auth = apiKeys.ThisOrDefault(); - Completions = new CompletionEndpoint(this); - Models = new ModelsEndpoint(this); - Files = new FilesEndpoint(this); - Embeddings = new EmbeddingEndpoint(this); + this.Client = httpClient; + this.Completions = new CompletionEndpoint(this); + this.Models = new ModelsEndpoint(this); + this.Files = new FilesEndpoint(this); + this.Embeddings = new EmbeddingEndpoint(this); } /// @@ -52,8 +53,5 @@ public OpenAIAPI(APIAuthentication apiKeys = null) /// 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/OpenAIApiExtensions.cs b/OpenAI_API/OpenAIApiExtensions.cs new file mode 100644 index 0000000..c87d05d --- /dev/null +++ b/OpenAI_API/OpenAIApiExtensions.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using OpenAI_API; + +static class OpenAIApiExtensions +{ + /// Register for DI services. Read configuration from appsettings "openAI": { "key": "", "org": "" } + /// + /// + /// + public static IServiceCollection AddOpenAIService(this IServiceCollection services, IConfiguration configuration) + { + var section = configuration.GetSection("openAI"); + if (!section.Exists()) return services; + + string? key = section["key"]; + if (key is null) return services; + + string? organisation = section["org"]; + return services.AddOpenAIService(new APIAuthentication(key, organisation)); + } + + public static IServiceCollection AddOpenAIService(this IServiceCollection services, APIAuthentication auth) + { + services.AddHttpClient(client => + EndpointBase.ConfigureClient(client, auth)); + + return services; + } +} \ No newline at end of file diff --git a/OpenAI_API/OpenAI_API.csproj b/OpenAI_API/OpenAI_API.csproj index b1f8fec..c9c18da 100644 --- a/OpenAI_API/OpenAI_API.csproj +++ b/OpenAI_API/OpenAI_API.csproj @@ -1,8 +1,7 @@  - netstandard2.0 - 8.0 + net7.0 true OkGoDoIt (Roger Pincombe) OpenAI API @@ -30,7 +29,8 @@ snupkg true - + enable + latest @@ -41,7 +41,10 @@ - + + + + From 6c5c05f1cc25d98e6d3f6d3c34147694f857c224 Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Wed, 15 Feb 2023 11:56:48 +0000 Subject: [PATCH 4/9] fix: dispose response/request --- OpenAI_API/EndpointBase.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs index a8fe1c0..1105f33 100644 --- a/OpenAI_API/EndpointBase.cs +++ b/OpenAI_API/EndpointBase.cs @@ -96,9 +96,8 @@ private async Task HttpRequestRaw(string url = null, HttpMe if (verb == null) verb = HttpMethod.Get; - HttpResponseMessage response = null; - string resultAsString = null; - HttpRequestMessage req = new HttpRequestMessage(verb, url); + + using var req = new HttpRequestMessage(verb, url); if (postData != null) { @@ -113,15 +112,16 @@ private async Task HttpRequestRaw(string url = null, HttpMe req.Content = stringContent; } } - response = await this._Api.Client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + using var response = await this._Api.Client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); if (response.IsSuccessStatusCode) { return response; } else { - try + string resultAsString = null; + try { resultAsString = await response.Content.ReadAsStringAsync(); } From 964be7700f18c96781f993a169d0be2bd3cde608 Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Wed, 15 Feb 2023 11:57:11 +0000 Subject: [PATCH 5/9] fix: make extension public --- OpenAI_API/OpenAIApiExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAI_API/OpenAIApiExtensions.cs b/OpenAI_API/OpenAIApiExtensions.cs index c87d05d..4374345 100644 --- a/OpenAI_API/OpenAIApiExtensions.cs +++ b/OpenAI_API/OpenAIApiExtensions.cs @@ -3,7 +3,7 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using OpenAI_API; -static class OpenAIApiExtensions +public static class OpenAIApiExtensions { /// Register for DI services. Read configuration from appsettings "openAI": { "key": "", "org": "" } /// From d23797c56371c1d0fb01040bfe5d830f81f02164 Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Wed, 15 Feb 2023 11:58:34 +0000 Subject: [PATCH 6/9] feat: tests use mock service builder --- OpenAI_Tests/AuthTests.cs | 86 ++++++++++++++----------- OpenAI_Tests/CompletionEndpointTests.cs | 44 ++++++------- OpenAI_Tests/EmbeddingEndpointTests.cs | 4 +- OpenAI_Tests/FilesEndpointTests.cs | 8 +-- OpenAI_Tests/ModelEndpointTests.cs | 26 ++++---- OpenAI_Tests/OpenAI_Tests.csproj | 4 +- 6 files changed, 94 insertions(+), 78 deletions(-) diff --git a/OpenAI_Tests/AuthTests.cs b/OpenAI_Tests/AuthTests.cs index e791c81..293c33b 100644 --- a/OpenAI_Tests/AuthTests.cs +++ b/OpenAI_Tests/AuthTests.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using OpenAI_API; using System; using System.IO; using System.Threading.Tasks; @@ -59,26 +62,24 @@ public void GetDefault() } - - - [Test] - public void testHelper() - { - OpenAI_API.APIAuthentication defaultAuth = OpenAI_API.APIAuthentication.Default; - OpenAI_API.APIAuthentication manualAuth = new OpenAI_API.APIAuthentication("pk-testAA"); - OpenAI_API.OpenAIAPI api = new OpenAI_API.OpenAIAPI(); - OpenAI_API.APIAuthentication shouldBeDefaultAuth = api.Auth; - Assert.IsNotNull(shouldBeDefaultAuth); - Assert.IsNotNull(shouldBeDefaultAuth.ApiKey); - Assert.AreEqual(defaultAuth.ApiKey, shouldBeDefaultAuth.ApiKey); - - OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication("pk-testAA"); - api = new OpenAI_API.OpenAIAPI(); - OpenAI_API.APIAuthentication shouldBeManualAuth = api.Auth; - Assert.IsNotNull(shouldBeManualAuth); - Assert.IsNotNull(shouldBeManualAuth.ApiKey); - Assert.AreEqual(manualAuth.ApiKey, shouldBeManualAuth.ApiKey); - } + // [Test] + // public void testHelper() + // { + // OpenAI_API.APIAuthentication defaultAuth = OpenAI_API.APIAuthentication.Default; + // OpenAI_API.APIAuthentication manualAuth = new OpenAI_API.APIAuthentication("pk-testAA"); + // OpenAI_API.OpenAIAPI api = AuthTests.InitService(); + // OpenAI_API.APIAuthentication shouldBeDefaultAuth = api.Auth; + // Assert.IsNotNull(shouldBeDefaultAuth); + // Assert.IsNotNull(shouldBeDefaultAuth.ApiKey); + // Assert.AreEqual(defaultAuth.ApiKey, shouldBeDefaultAuth.ApiKey); + // + // OpenAI_API.APIAuthentication.Default = new OpenAI_API.APIAuthentication("pk-testAA"); + // api = AuthTests.InitService(); + // OpenAI_API.APIAuthentication shouldBeManualAuth = api.Auth; + // Assert.IsNotNull(shouldBeManualAuth); + // Assert.IsNotNull(shouldBeManualAuth.ApiKey); + // Assert.AreEqual(manualAuth.ApiKey, shouldBeManualAuth.ApiKey); + // } [Test] public void GetKey() @@ -106,22 +107,35 @@ public void ParseKey() Assert.AreEqual("orgTest", auth.OpenAIOrganization); } - [Test] - public async Task TestBadKey() - { - var auth = new OpenAI_API.APIAuthentication("pk-testAA"); - Assert.IsFalse(await auth.ValidateAPIKey()); - - auth = new OpenAI_API.APIAuthentication(null); - Assert.IsFalse(await auth.ValidateAPIKey()); - } - - [Test] - public async Task TestValidateGoodKey() - { - var auth = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); - Assert.IsTrue(await auth.ValidateAPIKey()); - } + // [Test] + // public async Task TestBadKey() + // { + // var auth = new OpenAI_API.APIAuthentication("pk-testAA"); + // Assert.IsFalse(await auth.ValidateAPIKey()); + // + // auth = new OpenAI_API.APIAuthentication(null); + // Assert.IsFalse(await auth.ValidateAPIKey()); + // } + // + // [Test] + // public async Task TestValidateGoodKey() + // { + // var auth = new OpenAI_API.APIAuthentication(Environment.GetEnvironmentVariable("TEST_OPENAI_SECRET_KEY")); + // Assert.IsTrue(await auth.ValidateAPIKey()); + // } + + internal static IOpenAI InitService() { + IConfiguration config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", false, true) + .Build(); + + var services = new ServiceCollection(); + services.AddOpenAIService(config); + + var serviceProvider = services.BuildServiceProvider(); + + return serviceProvider.GetService(); + } } } \ No newline at end of file diff --git a/OpenAI_Tests/CompletionEndpointTests.cs b/OpenAI_Tests/CompletionEndpointTests.cs index 936cecb..40d4266 100644 --- a/OpenAI_Tests/CompletionEndpointTests.cs +++ b/OpenAI_Tests/CompletionEndpointTests.cs @@ -21,7 +21,7 @@ public void Setup() [Test] public void GetBasicCompletion() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); Assert.IsNotNull(api.Completions); @@ -41,9 +41,9 @@ public void GetBasicCompletion() [Test] public void GetSimpleCompletion() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); - Assert.IsNotNull(api.Completions); + 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); @@ -56,9 +56,9 @@ public void GetSimpleCompletion() [Test] public async Task CreateCompletionAsync_MultiplePrompts_ShouldReturnResult() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); - var completionReq = new CompletionRequest + var completionReq = new CompletionRequest { MultiplePrompts = new[] { @@ -80,9 +80,9 @@ public async Task CreateCompletionAsync_MultiplePrompts_ShouldReturnResult() [TestCase(3)] public void CreateCompletionAsync_ShouldNotAllowTemperatureOutside01(double temperature) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); - var completionReq = new CompletionRequest + var completionReq = new CompletionRequest { Prompt = "three four five", Temperature = temperature, @@ -100,9 +100,9 @@ public void CreateCompletionAsync_ShouldNotAllowTemperatureOutside01(double temp [TestCase(2.0)] public async Task ShouldBeMoreCreativeWithHighTemperature(double temperature) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); - var completionReq = new CompletionRequest + var completionReq = new CompletionRequest { Prompt = "three four five", Temperature = temperature, @@ -119,9 +119,9 @@ public async Task ShouldBeMoreCreativeWithHighTemperature(double temperature) [TestCase(0.1)] public async Task CreateCompletionAsync_ShouldGetSomeResultsWithVariousTopPValues(double topP) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); - var completionReq = new CompletionRequest + var completionReq = new CompletionRequest { Prompt = "three four five", Temperature = 0, @@ -140,7 +140,7 @@ public async Task CreateCompletionAsync_ShouldGetSomeResultsWithVariousTopPValue [TestCase(1.0)] public async Task CreateCompletionAsync_ShouldReturnSomeResultsForPresencePenalty(double presencePenalty) { - var api = new OpenAI_API.OpenAIAPI(); + var api =AuthTests.InitService(); var completionReq = new CompletionRequest { @@ -161,7 +161,7 @@ public async Task CreateCompletionAsync_ShouldReturnSomeResultsForPresencePenalt [TestCase(1.0)] public async Task CreateCompletionAsync_ShouldReturnSomeResultsForFrequencyPenalty(double frequencyPenalty) { - var api = new OpenAI_API.OpenAIAPI(); + var api =AuthTests.InitService(); var completionReq = new CompletionRequest { @@ -179,7 +179,7 @@ public async Task CreateCompletionAsync_ShouldReturnSomeResultsForFrequencyPenal [Test] public async Task CreateCompletionAsync_ShouldWorkForBiggerNumberOfCompletions() { - var api = new OpenAI_API.OpenAIAPI(); + var api =AuthTests.InitService(); var completionReq = new CompletionRequest { @@ -199,7 +199,7 @@ public async Task CreateCompletionAsync_ShouldWorkForBiggerNumberOfCompletions() [TestCase(5)] public async Task CreateCompletionAsync_ShouldAlsoReturnLogProps(int logProps) { - var api = new OpenAI_API.OpenAIAPI(); + var api =AuthTests.InitService(); var completionReq = new CompletionRequest { @@ -221,7 +221,7 @@ public async Task CreateCompletionAsync_ShouldAlsoReturnLogProps(int logProps) [Test] public async Task CreateCompletionAsync_Echo_ShouldReturnTheInput() { - var api = new OpenAI_API.OpenAIAPI(); + var api =AuthTests.InitService(); var completionReq = new CompletionRequest { @@ -240,7 +240,7 @@ public async Task CreateCompletionAsync_Echo_ShouldReturnTheInput() [TestCase("Friday")] public async Task CreateCompletionAsync_ShouldStopOnStopSequence(string stopSeq) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var completionReq = new CompletionRequest { @@ -260,7 +260,7 @@ public async Task CreateCompletionAsync_ShouldStopOnStopSequence(string stopSeq) [Test] public async Task CreateCompletionAsync_MultipleParamShouldReturnTheSameDataAsSingleParamVersion() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var r = new CompletionRequest { @@ -296,7 +296,7 @@ public async Task CreateCompletionAsync_MultipleParamShouldReturnTheSameDataAsSi [TestCase(7, 2)] public async Task StreamCompletionAsync_ShouldStreamIndexAndData(int maxTokens, int numOutputs) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var completionRequest = new CompletionRequest { @@ -328,7 +328,7 @@ await api.Completions.StreamCompletionAsync(completionRequest, (index, result) = [TestCase(7, 2)] public async Task StreamCompletionAsync_ShouldStreamData(int maxTokens, int numOutputs) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var completionRequest = new CompletionRequest { @@ -357,7 +357,7 @@ await api.Completions.StreamCompletionAsync(completionRequest, result => [TestCase(7, 2)] public async Task StreamCompletionEnumerableAsync_ShouldStreamData(int maxTokens, int numOutputs) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var completionRequest = new CompletionRequest { @@ -385,7 +385,7 @@ public async Task StreamCompletionEnumerableAsync_ShouldStreamData(int maxTokens [Test] public async Task StreamCompletionEnumerableAsync_MultipleParamShouldReturnTheSameDataAsSingleParamVersion() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var r = new CompletionRequest { diff --git a/OpenAI_Tests/EmbeddingEndpointTests.cs b/OpenAI_Tests/EmbeddingEndpointTests.cs index ee58a57..cd221d6 100644 --- a/OpenAI_Tests/EmbeddingEndpointTests.cs +++ b/OpenAI_Tests/EmbeddingEndpointTests.cs @@ -17,7 +17,7 @@ public void Setup() [Test] public void GetBasicEmbedding() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); Assert.IsNotNull(api.Embeddings); @@ -41,7 +41,7 @@ public void GetBasicEmbedding() [Test] public void GetSimpleEmbedding() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); Assert.IsNotNull(api.Embeddings); diff --git a/OpenAI_Tests/FilesEndpointTests.cs b/OpenAI_Tests/FilesEndpointTests.cs index 6cf3df8..00fd1b8 100644 --- a/OpenAI_Tests/FilesEndpointTests.cs +++ b/OpenAI_Tests/FilesEndpointTests.cs @@ -17,7 +17,7 @@ public void Setup() [Order(1)] public async Task UploadFile() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var response = await api.Files.UploadFileAsync("fine-tuning-data.jsonl"); Assert.IsNotNull(response); Assert.IsTrue(response.Id.Length > 0); @@ -33,7 +33,7 @@ public async Task UploadFile() [Order(2)] public async Task ListFiles() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var response = await api.Files.GetFilesAsync(); foreach (var file in response) @@ -48,7 +48,7 @@ public async Task ListFiles() [Order(3)] public async Task GetFile() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var response = await api.Files.GetFilesAsync(); foreach (var file in response) { @@ -68,7 +68,7 @@ public async Task GetFile() [Order(4)] public async Task DeleteFiles() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); var response = await api.Files.GetFilesAsync(); foreach (var file in response) { diff --git a/OpenAI_Tests/ModelEndpointTests.cs b/OpenAI_Tests/ModelEndpointTests.cs index 3ccfef6..78dad3f 100644 --- a/OpenAI_Tests/ModelEndpointTests.cs +++ b/OpenAI_Tests/ModelEndpointTests.cs @@ -20,7 +20,7 @@ public void Setup() [Test] public void GetAllModels() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); Assert.IsNotNull(api.Models); @@ -33,7 +33,7 @@ public void GetAllModels() [Test] public void GetModelDetails() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); Assert.IsNotNull(api.Models); @@ -55,20 +55,20 @@ public void GetModelDetails() [Test] public async Task GetEnginesAsync_ShouldReturnTheEngineList() { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); 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")); - } + // [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")] @@ -76,7 +76,7 @@ public void GetEnginesAsync_ShouldFailIfInvalidAuthIsProvided() [TestCase("davinci")] public async Task RetrieveEngineDetailsAsync_ShouldRetrieveEngineDetails(string modelId) { - var api = new OpenAI_API.OpenAIAPI(); + var api = AuthTests.InitService(); 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"); diff --git a/OpenAI_Tests/OpenAI_Tests.csproj b/OpenAI_Tests/OpenAI_Tests.csproj index 805cc44..57ca2cd 100644 --- a/OpenAI_Tests/OpenAI_Tests.csproj +++ b/OpenAI_Tests/OpenAI_Tests.csproj @@ -1,13 +1,15 @@ - netcoreapp3.1 + net7.0 false + + From 0b98925e9f95242e9aa4492ca625ad16cb7029e7 Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Wed, 15 Feb 2023 19:12:12 +0000 Subject: [PATCH 7/9] fix: premature disposal responses should be disposed, but as this returns the response it will dispose twice --- OpenAI_API/EndpointBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs index 1105f33..e096223 100644 --- a/OpenAI_API/EndpointBase.cs +++ b/OpenAI_API/EndpointBase.cs @@ -113,7 +113,7 @@ private async Task HttpRequestRaw(string url = null, HttpMe } } - using var response = await this._Api.Client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + var response = await this._Api.Client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); if (response.IsSuccessStatusCode) { return response; From dafb02e0f9bb2e14e3bf4e9f6585917c92782856 Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Fri, 17 Feb 2023 10:13:48 +0000 Subject: [PATCH 8/9] feat: backwards-compatible constructors --- OpenAI_API/OpenAIAPI.cs | 51 ++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/OpenAI_API/OpenAIAPI.cs b/OpenAI_API/OpenAIAPI.cs index de7c164..6bf9555 100644 --- a/OpenAI_API/OpenAIAPI.cs +++ b/OpenAI_API/OpenAIAPI.cs @@ -20,24 +20,49 @@ public class OpenAIAPI: IOpenAI /// The HTTP client configured with the authentication headers. internal HttpClient Client { get; } + private OpenAIAPI() + { + this.Completions = new CompletionEndpoint(this); + this.Models = new ModelsEndpoint(this); + this.Files = new FilesEndpoint(this); + this.Embeddings = new EmbeddingEndpoint(this); + } + /// /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints /// /// The HTTP client configured with the authentication headers. - public OpenAIAPI( - HttpClient httpClient) - { - this.Client = httpClient; - this.Completions = new CompletionEndpoint(this); - this.Models = new ModelsEndpoint(this); - this.Files = new FilesEndpoint(this); - this.Embeddings = new EmbeddingEndpoint(this); - } + public OpenAIAPI(HttpClient httpClient) : this() => + this.Client = httpClient; - /// - /// 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; } + /// + /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints + /// + /// The authentication details for the API + [Obsolete(""" + This constructor will generate a new HTTP client every time it is called. + Do not use this in scenarios where multiple instances of the API are required. + This is provided for backwards compatibility, use .NET Dependency Injection instead. + """, false)] + public OpenAIAPI(APIAuthentication auth) : + this(EndpointBase.ConfigureClient(new HttpClient(), auth)) { } + + /// + /// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints + /// + /// The authentication key for the API + [Obsolete(""" + This constructor will generate a new HTTP client every time it is called. + Do not use this in scenarios where multiple instances of the API are required. + This is provided for backwards compatibility, use .NET Dependency Injection instead. + """, false)] + public OpenAIAPI(string key) : + this(EndpointBase.ConfigureClient(new HttpClient(), new APIAuthentication(key))) { } + + /// + /// 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. From 43370bdff30833a417b82c494a214ee81aad63f6 Mon Sep 17 00:00:00 2001 From: Keith Henry Date: Fri, 17 Feb 2023 10:22:03 +0000 Subject: [PATCH 9/9] docs: DI --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b490204..8b73a01 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,36 @@ Install package [`OpenAI` from Nuget](https://www.nuget.org/packages/OpenAI/). Install-Package OpenAI ``` -### Authentication +### Register for Dependency Injection +If you are using Dependency Injection, you can register the API using `appsettings.json` configuration: + +```json +"openAI": { + "key": "YOUR_API_KEY", + "org": "OPENAI_ORGANIZATION" +} +``` + +Then in your `Program.cs` or `Startup.cs` add: + +```c# +builder.Services.AddOpenAIService(builder.Configuration); +``` + +This registers `IOpenAI` for dependency injection using: +- `[FromServices] IOpenAI? openAI` in your controllers. +- `IOpenAI? openAI` in your constructor. + +#### Legacy constructors +Previous versions of this package allowed direct instantiation of the service, but this causes connection pool exhaustion when multiple instances exist. + +It is still supported for backwards compatibility, but not recommended. + There are 3 ways to provide your API keys, in order of precedence: 1. Pass keys directly to `APIAuthentication(string key)` constructor 2. Set environment var for OPENAI_API_KEY (or OPENAI_KEY for backwards compatibility) 3. Include a config file in the local directory or in your user directory named `.openai` and containing the line: + ```shell OPENAI_API_KEY=sk-aaaabbbbbccccddddd ```