Skip to content

Commit

Permalink
OpenAI-DotNet 8.4.0 (#375)
Browse files Browse the repository at this point in the history
- Add realtime support
- Added `o1`, `o1-mini`, `gpt-4o-mini`, and `gpt-4o-realtime`, `gpt-4o-audio` model convenience properties
- Fixed some bugs with function invocations
- Fixed `strict` for built in `FunctionAttribute` defined tools
- Fixed `FunctionAttribute` tool generated names so they aren't too long
- Refactored `Tools` and `ToolCalls`. There is more of a distinction now in `ChatResponses`
- Refactored `SpeechRequest`, and deprecated `SpeechVoice` enum in favor of new `Voice` class
- Refactored `OpenAI.Chat` to support new audio modalities and output audio
  • Loading branch information
StephenHodgson authored Nov 15, 2024
1 parent b1aba5d commit 0d4ee48
Show file tree
Hide file tree
Showing 123 changed files with 4,962 additions and 987 deletions.
4 changes: 3 additions & 1 deletion OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
<IncludeSymbols>true</IncludeSymbols>
<SignAssembly>false</SignAssembly>
<ImplicitUsings>false</ImplicitUsings>
<Version>8.2.0</Version>
<Version>8.4.0</Version>
<PackageReleaseNotes>
Version 8.4.0
- Added support for Realtime Websocket proxy forwarding
Version 8.2.0
- Deprecated ValidateAuthentication for ValidateAuthenticationAsync
Version 8.1.1
Expand Down
4 changes: 0 additions & 4 deletions OpenAI-DotNet-Proxy/Proxy/AbstractAuthenticationFilter.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;

namespace OpenAI.Proxy
{
/// <inheritdoc />
public abstract class AbstractAuthenticationFilter : IAuthenticationFilter
{
[Obsolete("Use ValidateAuthenticationAsync")]
public virtual void ValidateAuthentication(IHeaderDictionary request) { }

/// <inheritdoc />
public abstract Task ValidateAuthenticationAsync(IHeaderDictionary request);
}
Expand Down
128 changes: 112 additions & 16 deletions OpenAI-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Security.Authentication;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace OpenAI.Proxy
Expand All @@ -24,6 +26,9 @@ public static class EndpointRouteBuilder
HeaderNames.TransferEncoding,
HeaderNames.KeepAlive,
HeaderNames.Upgrade,
HeaderNames.Host,
HeaderNames.SecWebSocketKey,
HeaderNames.SecWebSocketVersion,
"Proxy-Connection",
"Proxy-Authenticate",
"Proxy-Authentication-Info",
Expand Down Expand Up @@ -52,22 +57,25 @@ public static class EndpointRouteBuilder
public static void MapOpenAIEndpoints(this IEndpointRouteBuilder endpoints, OpenAIClient openAIClient, IAuthenticationFilter authenticationFilter, string routePrefix = "")
{
endpoints.Map($"{routePrefix}{openAIClient.OpenAIClientSettings.BaseRequest}{{**endpoint}}", HandleRequest);
return;

async Task HandleRequest(HttpContext httpContext, string endpoint)
{
try
{
#pragma warning disable CS0618 // Type or member is obsolete
// ReSharper disable once MethodHasAsyncOverload
authenticationFilter.ValidateAuthentication(httpContext.Request.Headers);
#pragma warning restore CS0618 // Type or member is obsolete
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers);
if (httpContext.WebSockets.IsWebSocketRequest)
{
await ProcessWebSocketRequest(httpContext, endpoint).ConfigureAwait(false);
return;
}

await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers).ConfigureAwait(false);
var method = new HttpMethod(httpContext.Request.Method);

var uri = new Uri(string.Format(
openAIClient.OpenAIClientSettings.BaseRequestUrlFormat,
$"{endpoint}{httpContext.Request.QueryString}"
));
openAIClient.OpenAIClientSettings.BaseRequestUrlFormat,
$"{endpoint}{httpContext.Request.QueryString}"
));
using var request = new HttpRequestMessage(method, uri);
request.Content = new StreamContent(httpContext.Request.Body);

Expand All @@ -76,7 +84,7 @@ async Task HandleRequest(HttpContext httpContext, string endpoint)
request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(httpContext.Request.ContentType);
}

var proxyResponse = await openAIClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
var proxyResponse = await openAIClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, httpContext.RequestAborted).ConfigureAwait(false);
httpContext.Response.StatusCode = (int)proxyResponse.StatusCode;

foreach (var (key, value) in proxyResponse.Headers)
Expand All @@ -96,32 +104,120 @@ async Task HandleRequest(HttpContext httpContext, string endpoint)

if (httpContext.Response.ContentType.Equals(streamingContent))
{
var stream = await proxyResponse.Content.ReadAsStreamAsync();
await WriteServerStreamEventsAsync(httpContext, stream);
var stream = await proxyResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
await WriteServerStreamEventsAsync(httpContext, stream).ConfigureAwait(false);
}
else
{
await proxyResponse.Content.CopyToAsync(httpContext.Response.Body);
await proxyResponse.Content.CopyToAsync(httpContext.Response.Body, httpContext.RequestAborted).ConfigureAwait(false);
}
}
catch (AuthenticationException authenticationException)
{
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
await httpContext.Response.WriteAsync(authenticationException.Message);
await httpContext.Response.WriteAsync(authenticationException.Message).ConfigureAwait(false);
}
catch (WebSocketException)
{
// ignore
throw;
}
catch (Exception e)
{
if (httpContext.Response.HasStarted) { throw; }
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = JsonSerializer.Serialize(new { error = new { e.Message, e.StackTrace } });
await httpContext.Response.WriteAsync(response);
await httpContext.Response.WriteAsync(response).ConfigureAwait(false);
}

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

async Task ProcessWebSocketRequest(HttpContext httpContext, string endpoint)
{
using var clientWebsocket = await httpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);

try
{
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers).ConfigureAwait(false);
}
catch (AuthenticationException authenticationException)
{
var message = JsonSerializer.Serialize(new
{
type = "error",
error = new
{
type = "invalid_request_error",
code = "invalid_session_token",
message = authenticationException.Message
}
});
await clientWebsocket.SendAsync(System.Text.Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, httpContext.RequestAborted).ConfigureAwait(false);
await clientWebsocket.CloseAsync(WebSocketCloseStatus.PolicyViolation, authenticationException.Message, httpContext.RequestAborted).ConfigureAwait(false);
return;
}

if (endpoint.EndsWith("echo"))
{
await EchoAsync(clientWebsocket, httpContext.RequestAborted);
return;
}

using var hostWebsocket = new ClientWebSocket();

foreach (var header in openAIClient.WebsocketHeaders)
{
hostWebsocket.Options.SetRequestHeader(header.Key, header.Value);
}

var uri = new Uri(string.Format(
openAIClient.OpenAIClientSettings.BaseWebSocketUrlFormat,
$"{endpoint}{httpContext.Request.QueryString}"
));
await hostWebsocket.ConnectAsync(uri, httpContext.RequestAborted).ConfigureAwait(false);
var receive = ProxyWebSocketMessages(clientWebsocket, hostWebsocket, httpContext.RequestAborted);
var send = ProxyWebSocketMessages(hostWebsocket, clientWebsocket, httpContext.RequestAborted);
await Task.WhenAll(receive, send).ConfigureAwait(false);
return;

async Task ProxyWebSocketMessages(WebSocket fromSocket, WebSocket toSocket, CancellationToken cancellationToken)
{
var buffer = new byte[1024 * 4];
var memoryBuffer = buffer.AsMemory();

while (fromSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
var result = await fromSocket.ReceiveAsync(memoryBuffer, cancellationToken).ConfigureAwait(false);

if (fromSocket.CloseStatus.HasValue || result.MessageType == WebSocketMessageType.Close)
{
await toSocket.CloseOutputAsync(fromSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, fromSocket.CloseStatusDescription ?? "Closing", cancellationToken).ConfigureAwait(false);
break;
}

await toSocket.SendAsync(memoryBuffer[..result.Count], result.MessageType, result.EndOfMessage, cancellationToken).ConfigureAwait(false);
}
}
}

static async Task EchoAsync(WebSocket webSocket, CancellationToken cancellationToken)
{
var buffer = new byte[1024 * 4];
var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);

while (!receiveResult.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, receiveResult.Count), receiveResult.MessageType, receiveResult.EndOfMessage, cancellationToken);
receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
}

await webSocket.CloseAsync(receiveResult.CloseStatus.Value, receiveResult.CloseStatusDescription, cancellationToken);
}
}
}
Expand Down
4 changes: 0 additions & 4 deletions OpenAI-DotNet-Proxy/Proxy/IAuthenticationFilter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.AspNetCore.Http;
using System;
using System.Security.Authentication;
using System.Threading.Tasks;

Expand All @@ -12,9 +11,6 @@ namespace OpenAI.Proxy
/// </summary>
public interface IAuthenticationFilter
{
[Obsolete("Use ValidateAuthenticationAsync")]
void ValidateAuthentication(IHeaderDictionary request);

/// <summary>
/// Checks the headers for your user issued token.
/// If it's not valid, then throw <see cref="AuthenticationException"/>.
Expand Down
11 changes: 11 additions & 0 deletions OpenAI-DotNet-Proxy/Proxy/OpenAIProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

Expand Down Expand Up @@ -41,6 +42,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
SetupServices(app.ApplicationServices);

app.UseHttpsRedirection();
app.UseWebSockets();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
Expand All @@ -62,6 +64,12 @@ public static IHost CreateDefaultHost<T>(string[] args, OpenAIClient openAIClien
webBuilder.UseStartup<OpenAIProxy>();
webBuilder.ConfigureKestrel(ConfigureKestrel);
})
.ConfigureLogging(logger =>
{
logger.ClearProviders();
logger.AddConsole();
logger.SetMinimumLevel(LogLevel.Debug);
})
.ConfigureServices(services =>
{
services.AddSingleton(openAIClient);
Expand All @@ -77,6 +85,9 @@ public static IHost CreateDefaultHost<T>(string[] args, OpenAIClient openAIClien
public static WebApplication CreateWebApplication<T>(string[] args, OpenAIClient openAIClient) where T : class, IAuthenticationFilter
{
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
builder.WebHost.ConfigureKestrel(ConfigureKestrel);
builder.Services.AddSingleton(openAIClient);
builder.Services.AddSingleton<IAuthenticationFilter, T>();
Expand Down
30 changes: 0 additions & 30 deletions OpenAI-DotNet-Proxy/Proxy/OpenAIProxyStartup.cs

This file was deleted.

6 changes: 6 additions & 0 deletions OpenAI-DotNet-Tests-Proxy/OpenAI-DotNet-Tests-Proxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenAI-DotNet-Proxy\OpenAI-DotNet-Proxy.csproj" />
<ProjectReference Include="..\OpenAI-DotNet\OpenAI-DotNet.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Properties\launchSettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion OpenAI-DotNet-Tests-Proxy/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public static void Main(string[] args)
var auth = OpenAIAuthentication.LoadFromEnv();
var settings = new OpenAIClientSettings(/* your custom settings if using Azure OpenAI */);
using var openAIClient = new OpenAIClient(auth, settings);
OpenAIProxy.CreateWebApplication<AuthenticationFilter>(args, openAIClient).Run();
using var app = OpenAIProxy.CreateWebApplication<AuthenticationFilter>(args, openAIClient);
app.Run();
}
}
}
2 changes: 1 addition & 1 deletion OpenAI-DotNet-Tests-Proxy/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
}
}
}
}
}
8 changes: 0 additions & 8 deletions OpenAI-DotNet-Tests-Proxy/appsettings.Development.json

This file was deleted.

Loading

0 comments on commit 0d4ee48

Please sign in to comment.