Skip to content

Commit

Permalink
OpenAI-DotNet 6.2.0 (RageAgainstThePixel#46)
Browse files Browse the repository at this point in the history
- Added OpenAI-DotNet-Proxy project and package.
- Added support for custom domains
- Updated unit tests
- Updated docs
  • Loading branch information
StephenHodgson committed Mar 16, 2023
1 parent cbfda27 commit 5228434
Show file tree
Hide file tree
Showing 36 changed files with 882 additions and 234 deletions.
41 changes: 41 additions & 0 deletions .github/actions/build-and-publish/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# .github/actions/build-and-publish/action.yml
name: "Build and Publish NuGet Package"
description: "Build and publish the specified NuGet package"
inputs:
project_name:
description: "The name of the project"
required: true
nuget_api_key:
description: "NuGet API Key"
required: true
runs:
using: "composite"
steps:
- name: Build Pack and Publish NuGet Package
run: |
$projectPath = "${{ github.workspace }}\${{ inputs.project_name }}"
dotnet build $projectPath --configuration Release
$out = "$projectPath\bin\Release"
$packagePath = Get-ChildItem -Path $out -File -Include '*.nupkg' -Recurse -ErrorAction SilentlyContinue
if ($packagePath) {
Write-Host Package path: $packagePath
} else {
Write-Host Failed to find package at $out
exit 1
}
$isRelease = "${{ github.ref == 'refs/heads/main' }}"
if ($isRelease -eq 'true') {
dotnet nuget push $packagePath --api-key ${{ inputs.nuget_api_key }} --source https://api.nuget.org/v3/index.json
}
$version = $packagePath.Name -replace "^${{ inputs.project_name }}.(.*).nupkg$",'$1'
echo "PACKAGE_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
shell: pwsh

- uses: actions/upload-artifact@v3
with:
name: ${{ inputs.project_name }}.${{ env.PACKAGE_VERSION }}
path: ${{ github.workspace }}/${{ inputs.project_name }}/bin/Release/${{ inputs.project_name }}.${{ env.PACKAGE_VERSION }}.nupkg
85 changes: 49 additions & 36 deletions .github/workflows/Publish-Nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ on:
push:
branches:
- main
paths:
- OpenAI-DotNet/**
pull_request:
branches:
- '*'
paths:
- OpenAI-DotNet/**

workflow_dispatch:
inputs:
dotnet-version:
description: ".NET version to use"
required: false
default: "6.0.x"

jobs:
build:
strategy:
matrix:
configuration: [Release]
env:
DOTNET_VERSION: ${{ github.event.inputs.dotnet-version || '6.0.x' }}

jobs:
test:
runs-on: windows-latest

steps:
Expand All @@ -29,40 +29,53 @@ jobs:

- uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'

- uses: microsoft/setup-msbuild@v1
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Test Package ${{ matrix.configuration }}
run: dotnet test --configuration ${{ matrix.configuration }}
- name: Test Packages
run: dotnet test --configuration Release
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_ORGANIZATION_ID: ${{ secrets.OPENAI_ORGANIZATION_ID }}

- name: Publish Nuget Package
run: |
$out = "${{ github.workspace }}\OpenAI-DotNet\bin\${{ matrix.configuration }}"
- name: No tests needed
run: echo "No tests needed as no files in the specified paths were modified."

if (Test-Path -Path $out) {
$packagePath = Get-ChildItem -Path $out -File -Include '*.nupkg' -Recurse
}
build-openai-dotnet:
needs: test
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

if (Test-Path -Path $packagePath) {
Write-Host Package path: $packagePath
} else {
Write-Host Failed to find package at $packagePath
exit 1
}
- uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

$isRelease = "${{ github.ref == 'refs/heads/main' && matrix.configuration == 'Release' }}"
- uses: microsoft/setup-msbuild@v1

if ($isRelease -eq 'true') {
dotnet nuget push $packagePath --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
}
shell: pwsh
- name: Build and Publish OpenAI-DotNet NuGet Package
uses: ./.github/actions/build-and-publish
with:
project_name: OpenAI-DotNet
nuget_api_key: ${{ secrets.NUGET_API_KEY }}

- uses: actions/upload-artifact@v3
if: always()
with:
name: Nuget Package ${{ matrix.configuration }}
path: ${{ github.workspace }}\OpenAI-DotNet\bin
build-openai-dotnet-proxy:
needs: test
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- uses: microsoft/setup-msbuild@v1

- name: Build and Publish OpenAI-DotNet-Proxy NuGet Package
uses: ./.github/actions/build-and-publish
with:
project_name: OpenAI-DotNet-Proxy
nuget_api_key: ${{ secrets.NUGET_API_KEY }}
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<OpenAIDotNetVersion>6.2.0</OpenAIDotNetVersion>
</PropertyGroup>
</Project>
41 changes: 41 additions & 0 deletions OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<ImplicitUsings>false</ImplicitUsings>
<Nullable>disable</Nullable>
<SignAssembly>false</SignAssembly>
<Authors>Stephen Hodgson</Authors>
<Product>OpenAI-DotNet-Proxy</Product>
<Description>A simple Proxy API gateway for OpenAI-DotNet to make authenticated requests from a front end application without exposing your API keys.</Description>
<Copyright>2023</Copyright>
<PackageLicenseExpression>CC0-1.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/RageAgainstThePixel/OpenAI-DotNet</PackageProjectUrl>
<RepositoryUrl>https://github.com/RageAgainstThePixel/OpenAI-DotNet</RepositoryUrl>
<PackageTags>OpenAI, AI, ML, API, gpt, gpt-4, gpt-3.5-turbo, gpt-3, chatGPT, api-proxy, proxy, gateway</PackageTags>
<Title>OpenAI API Proxy</Title>
<PackageId>OpenAI-DotNet-Proxy</PackageId>
<Version>$(OpenAIDotNetVersion)</Version>
<Company>RageAgainstThePixel</Company>
<RootNamespace>OpenAI.Proxy</RootNamespace>
<PackageIcon>OpenAI-DotNet-Icon.png</PackageIcon>
<PackageReleaseNotes>Initial Release!</PackageReleaseNotes>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenAI-DotNet\OpenAI-DotNet.csproj" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<None Include="..\OpenAI-DotNet\Assets\OpenAI-DotNet-Icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions OpenAI-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Http;

namespace OpenAI.Proxy
{
/// <inheritdoc />
public abstract class AbstractAuthenticationFilter : IAuthenticationFilter
{
/// <inheritdoc />
public abstract void ValidateAuthentication(IHeaderDictionary request);
}
}
19 changes: 19 additions & 0 deletions OpenAI-DotNet-Proxy/Proxy/IAuthenticationFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Security.Authentication;
using Microsoft.AspNetCore.Http;

namespace OpenAI.Proxy
{
/// <summary>
/// Filters headers to ensure your users have the correct access.
/// </summary>
public interface IAuthenticationFilter
{
/// <summary>
/// Checks the headers for your user issued token.
/// If it's not valid, then throw <see cref="AuthenticationException"/>.
/// </summary>
/// <param name="request"></param>
/// <exception cref="AuthenticationException"></exception>
void ValidateAuthentication(IHeaderDictionary request);
}
}
145 changes: 145 additions & 0 deletions OpenAI-DotNet-Proxy/Proxy/OpenAIProxyStartup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Authentication;
using System.Threading.Tasks;

namespace OpenAI.Proxy
{
/// <summary>
/// Used in ASP.NET Core WebApps to start your own OpenAI web api proxy.
/// </summary>
public class OpenAIProxyStartup
{
private OpenAIClient openAIClient;
private IAuthenticationFilter authenticationFilter;

public void ConfigureServices(IServiceCollection services) { }

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

openAIClient = app.ApplicationServices.GetRequiredService<OpenAIClient>();
authenticationFilter = app.ApplicationServices.GetRequiredService<IAuthenticationFilter>();

app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/health", HealthEndpoint);
endpoints.Map($"{openAIClient.OpenAIClientSettings.BaseRequest}{{**endpoint}}", HandleRequest);
});
}

/// <summary>
/// Creates a new <see cref="IHost"/> that acts as a proxy web api for OpenAI.
/// </summary>
/// <typeparam name="T"><see cref="IAuthenticationFilter"/> type to use to validate your custom issued tokens.</typeparam>
/// <param name="args">Startup args.</param>
/// <param name="openAIClient"><see cref="OpenAIClient"/> with configured <see cref="OpenAIAuthentication"/> and <see cref="OpenAIClientSettings"/>.</param>
public static IHost CreateDefaultHost<T>(string[] args, OpenAIClient openAIClient) where T : class, IAuthenticationFilter
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<OpenAIProxyStartup>();
webBuilder.ConfigureKestrel(options =>
{
options.AllowSynchronousIO = false;
options.Limits.MinRequestBodyDataRate = null;
options.Limits.MinResponseDataRate = null;
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10);
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2);
});
})
.ConfigureServices(services =>
{
services.AddSingleton(openAIClient);
services.AddSingleton<IAuthenticationFilter, T>();
}).Build();
}

private static async Task HealthEndpoint(HttpContext context)
{
// Respond with a 200 OK status code and a plain text message
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync("OK");
}

/// <summary>
/// Handles incoming requests, validates authentication, and forwards the request to OpenAI API
/// </summary>
private async Task HandleRequest(HttpContext httpContext, string endpoint)
{
try
{
authenticationFilter.ValidateAuthentication(httpContext.Request.Headers);

var method = new HttpMethod(httpContext.Request.Method);
var uri = new Uri(string.Format(openAIClient.OpenAIClientSettings.BaseRequestUrlFormat, $"{endpoint}{httpContext.Request.QueryString}"));
var openAIRequest = new HttpRequestMessage(method, uri);

openAIRequest.Content = new StreamContent(httpContext.Request.Body);

if (httpContext.Request.ContentType != null)
{
openAIRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(httpContext.Request.ContentType);
}

var proxyResponse = await openAIClient.Client.SendAsync(openAIRequest, HttpCompletionOption.ResponseHeadersRead);
httpContext.Response.StatusCode = (int)proxyResponse.StatusCode;

foreach (var header in proxyResponse.Headers)
{
httpContext.Response.Headers[header.Key] = header.Value.ToArray();
}

foreach (var header in proxyResponse.Content.Headers)
{
httpContext.Response.Headers[header.Key] = header.Value.ToArray();
}

httpContext.Response.ContentType = proxyResponse.Content.Headers.ContentType?.ToString() ?? string.Empty;

if (httpContext.Response.ContentType.Equals("text/event-stream"))
{
var stream = await proxyResponse.Content.ReadAsStreamAsync();
await WriteServerStreamEventsAsync(httpContext, stream);
}
else
{
await proxyResponse.Content.CopyToAsync(httpContext.Response.Body);
}
}
catch (AuthenticationException authenticationException)
{
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
await httpContext.Response.WriteAsync(authenticationException.Message);
}
catch (Exception e)
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await httpContext.Response.WriteAsync(e.Message);
}
}

private static async Task WriteServerStreamEventsAsync(HttpContext httpContext, Stream contentStream)
{
var responseStream = httpContext.Response.Body;
await contentStream.CopyToAsync(responseStream, httpContext.RequestAborted);
await responseStream.FlushAsync(httpContext.RequestAborted);
}
}
}
Loading

0 comments on commit 5228434

Please sign in to comment.