Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Dependency Injection #44

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
28 changes: 0 additions & 28 deletions OpenAI_API/APIAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,34 +157,6 @@ public static APIAuthentication LoadFromPath(string directory = null, string fil

return new APIAuthentication(key, org);
}


/// <summary>
/// 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.
/// </summary>
/// <returns><see langword="true"/> if the api key is valid, or <see langword="false"/> if empty or not accepted by the OpenAI API.</returns>
public async Task<bool> ValidateAPIKey()
{
if (string.IsNullOrEmpty(ApiKey))
return false;

var api = new OpenAIAPI(this);

List<Models.Model> results;

try
{
results = await api.Models.GetModelsAsync();
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
return false;
}

return (results.Count > 0);
}

}

internal static class AuthHelpers
Expand Down
25 changes: 10 additions & 15 deletions OpenAI_API/EndpointBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,19 @@ protected string Url
}
}

/// <summary>
/// Gets an HTTPClient with the appropriate authorization and other headers set
/// </summary>
/// <summary>Configures an HttpClient with the appropriate authorization and other headers set</summary>
/// <returns>The fully initialized HttpClient</returns>
/// <exception cref="AuthenticationException">Thrown if there is no valid authentication. Please refer to <see href="https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication"/> for details.</exception>
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;
}
Expand Down Expand Up @@ -99,11 +96,8 @@ private async Task<HttpResponseMessage> 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);

using var req = new HttpRequestMessage(verb, url);

if (postData != null)
{
Expand All @@ -118,15 +112,16 @@ private async Task<HttpResponseMessage> HttpRequestRaw(string url = null, HttpMe
req.Content = stringContent;
}
}
response = await 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;
}
else
{
try
string resultAsString = null;
try
{
resultAsString = await response.Content.ReadAsStringAsync();
}
Expand Down
1 change: 0 additions & 1 deletion OpenAI_API/Files/FilesEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public async Task<File> DeleteFileAsync(string fileId)
/// <param name="purpose">The intendend purpose of the uploaded documents. Use "fine-tune" for Fine-tuning. This allows us to validate the format of the uploaded file.</param>
public async Task<File> UploadFileAsync(string filePath, string purpose = "fine-tune")
{
HttpClient client = GetClient();
var content = new MultipartFormDataContent
{
{ new StringContent(purpose), "purpose" },
Expand Down
23 changes: 23 additions & 0 deletions OpenAI_API/IOpenAI.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints</summary>
public interface IOpenAI
{
/// <summary>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).</summary>
public CompletionEndpoint Completions { get; }

/// <summary>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.</summary>
public EmbeddingEndpoint Embeddings { get; }

/// <summary>The API endpoint for querying available Engines/models</summary>
public ModelsEndpoint Models { get; }

/// <summary>The API lets you do operations with files. You can upload, delete or retrieve files. Files can be used for fine-tuning, search, etc.</summary>
public FilesEndpoint Files { get; }
}

19 changes: 9 additions & 10 deletions OpenAI_API/Model/ModelsEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ internal ModelsEndpoint(OpenAIAPI api) : base(api) { }
/// </summary>
/// <param name="id">The id/name of the model to get more details about</param>
/// <returns>Asynchronously returns the <see cref="Model"/> with all available properties</returns>
public Task<Model> RetrieveModelDetailsAsync(string id)
public async Task<Model> RetrieveModelDetailsAsync(string id)
{
return RetrieveModelDetailsAsync(id, _Api?.Auth);
}
string resultAsString = await HttpGetContent<JsonHelperRoot>($"{Url}/{id}");
var model = JsonConvert.DeserializeObject<Model>(resultAsString);
return model;
}

/// <summary>
/// List all models via the API
Expand All @@ -43,14 +45,11 @@ public async Task<List<Model>> GetModelsAsync()
/// Get details about a particular Model from the API, specifically properties such as <see cref="Model.OwnedBy"/> and permissions.
/// </summary>
/// <param name="id">The id/name of the model to get more details about</param>
/// <param name="auth">API authentication in order to call the API endpoint. If not specified, attempts to use a default.</param>
/// <param name="auth">Obsolete: IGNORED</param>
/// <returns>Asynchronously returns the <see cref="Model"/> with all available properties</returns>
public async Task<Model> RetrieveModelDetailsAsync(string id, APIAuthentication auth = null)
{
string resultAsString = await HttpGetContent<JsonHelperRoot>($"{Url}/{id}");
var model = JsonConvert.DeserializeObject<Model>(resultAsString);
return model;
}
[Obsolete("Use the overload without the APIAuthentication parameter instead, as it is ignored.", false)] // See #42
public Task<Model> RetrieveModelDetailsAsync(string id, APIAuthentication auth) =>
this.RetrieveModelDetailsAsync(id);

/// <summary>
/// A helper class to deserialize the JSON API responses. This should not be used directly.
Expand Down
77 changes: 50 additions & 27 deletions OpenAI_API/OpenAIAPI.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,68 @@
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
{
/// <summary>
/// Entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints
/// </summary>
public class OpenAIAPI
{
public class OpenAIAPI: IOpenAI
{
/// <summary>
/// Base url for OpenAI
/// </summary>
public string ApiUrlBase = "https://api.openai.com/v1/";

/// <summary>
/// The API authentication information to use for API calls
/// </summary>
public APIAuthentication Auth { get; set; }

/// <summary>
/// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints
/// </summary>
/// <param name="apiKeys">The API authentication information to use for API calls, or <see langword="null"/> to attempt to use the <see cref="APIAuthentication.Default"/>, potentially loading from environment vars or from a config file.</param>
public OpenAIAPI(APIAuthentication apiKeys = null)
{
this.Auth = apiKeys.ThisOrDefault();
Completions = new CompletionEndpoint(this);
Models = new ModelsEndpoint(this);
Files = new FilesEndpoint(this);
Embeddings = new EmbeddingEndpoint(this);
}
/// <summary>The HTTP client configured with the authentication headers.</summary>
internal HttpClient Client { get; }

/// <summary>
/// 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).
/// </summary>
public CompletionEndpoint Completions { get; }
private OpenAIAPI()
{
this.Completions = new CompletionEndpoint(this);
this.Models = new ModelsEndpoint(this);
this.Files = new FilesEndpoint(this);
this.Embeddings = new EmbeddingEndpoint(this);
}

/// <summary>
/// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints
/// </summary>
/// <param name="httpClient">The HTTP client configured with the authentication headers.</param>
public OpenAIAPI(HttpClient httpClient) : this() =>
this.Client = httpClient;

/// <summary>
/// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints
/// </summary>
/// <param name="auth">The authentication details for the API</param>
[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)) { }

/// <summary>
/// Creates a new entry point to the OpenAPI API, handling auth and allowing access to the various API endpoints
/// </summary>
/// <param name="key">The authentication key for the API</param>
[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))) { }
Comment on lines +35 to +60
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An idea to give you the backwards compatibility, but it's somewhat messy - alternate constructors that create an HttpClient when called and configure it, but that match the old patterns.

These will cause warnings for users upgrading still.


/// <summary>
/// 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).
/// </summary>
public CompletionEndpoint Completions { get; }

/// <summary>
/// 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.
Expand All @@ -52,8 +78,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.
/// </summary>
public FilesEndpoint Files { get; }



}
}
31 changes: 31 additions & 0 deletions OpenAI_API/OpenAIApiExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#nullable enable
namespace Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using OpenAI_API;

public static class OpenAIApiExtensions
{
/// <summary>Register <see cref="IOpenAI"/> for DI services. Read configuration from appsettings <code>"openAI": { "key": "", "org": "" }</code></summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
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));
}
Comment on lines +12 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With introducing DI one should maybe also think of introducing the Options Pattern.
This will introduce a AddProjectServices(this IServiceCollection services, Action<ApiAuthenticationOptions> configure)
This way you don't have to pass down the IConfiguration and instead can work directly with the object in question.

Within your Startup/Program.cs you can then use the IConfiguration.GetSection().Bind to pass down the actual options. This way the library won't depend on the IConfiguration library and one can use IOptions<T> to fetch the apikey and such from controllers.

@KeithHenry You've made a great starting point but i think this can be improved even more, do you mind if i helped you out here/there?


public static IServiceCollection AddOpenAIService(this IServiceCollection services, APIAuthentication auth)
{
services.AddHttpClient<IOpenAI, OpenAIAPI>(client =>
EndpointBase.ConfigureClient(client, auth));

return services;
}
}
11 changes: 7 additions & 4 deletions OpenAI_API/OpenAI_API.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<TargetFramework>net7.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>OkGoDoIt (Roger Pincombe)</Authors>
<Product>OpenAI API</Product>
Expand Down Expand Up @@ -30,7 +29,8 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<Deterministic>true</Deterministic>

<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand All @@ -41,7 +41,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
Expand Down
Loading