Skip to content

Commit 96540eb

Browse files
OpenAI-DotNet 8.8.0 (#472)
- Improved RealtimeSession websocket support for proxies - Proxy no longer handles the websocket connection directly, but instead initiates the connection to the OpenAI api directly using the ephemeral api key ## OpenAI-DotNet-Proxy 8.8.0 - Removed Websocket handling from the proxy
1 parent 59b0afb commit 96540eb

File tree

15 files changed

+120
-233
lines changed

15 files changed

+120
-233
lines changed

OpenAI-DotNet-Proxy/OpenAI-DotNet-Proxy.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
<IncludeSymbols>true</IncludeSymbols>
2323
<SignAssembly>false</SignAssembly>
2424
<ImplicitUsings>false</ImplicitUsings>
25-
<Version>8.7.4</Version>
25+
<Version>8.8.0</Version>
2626
<PackageReleaseNotes>
27+
Version 8.8.0
28+
- Removed Websocket handling from the proxy
2729
Version 8.7.4
2830
- Updated proxy support for the OpenAI-DotNet package
2931
- Ensure we're returning the full response message body and content length to the clients

OpenAI-DotNet-Proxy/Proxy/EndpointRouteBuilder.cs

Lines changed: 6 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using System.Net.WebSockets;
1414
using System.Security.Authentication;
1515
using System.Text.Json;
16-
using System.Threading;
1716
using System.Threading.Tasks;
1817

1918
namespace OpenAI.Proxy
@@ -66,8 +65,7 @@ async Task HandleRequest(HttpContext httpContext, string endpoint)
6665
{
6766
if (httpContext.WebSockets.IsWebSocketRequest)
6867
{
69-
await ProcessWebSocketRequest(httpContext, endpoint).ConfigureAwait(false);
70-
return;
68+
throw new InvalidOperationException("Websockets not supported");
7169
}
7270

7371
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers).ConfigureAwait(false);
@@ -87,9 +85,7 @@ async Task HandleRequest(HttpContext httpContext, string endpoint)
8785

8886
var uri = new Uri(string.Format(
8987
client.Settings.BaseRequestUrlFormat,
90-
QueryHelpers.AddQueryString(endpoint, modifiedQuery)
91-
));
92-
88+
QueryHelpers.AddQueryString(endpoint, modifiedQuery)));
9389
using var request = new HttpRequestMessage(method, uri);
9490
request.Content = new StreamContent(httpContext.Request.Body);
9591

@@ -129,16 +125,18 @@ async Task HandleRequest(HttpContext httpContext, string endpoint)
129125
}
130126
catch (AuthenticationException authenticationException)
131127
{
128+
Console.WriteLine($"{nameof(AuthenticationException)}: {authenticationException.Message}");
132129
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
133130
await httpContext.Response.WriteAsync(authenticationException.Message).ConfigureAwait(false);
134131
}
135-
catch (WebSocketException)
132+
catch (WebSocketException webEx)
136133
{
137-
// ignore
134+
Console.WriteLine($"{nameof(WebSocketException)} [{webEx.WebSocketErrorCode}] {webEx.Message}");
138135
throw;
139136
}
140137
catch (Exception e)
141138
{
139+
Console.WriteLine($"{nameof(Exception)}: {e.Message}");
142140
if (httpContext.Response.HasStarted) { throw; }
143141
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
144142
var response = JsonSerializer.Serialize(new { error = new { e.Message, e.StackTrace } });
@@ -152,88 +150,6 @@ static async Task WriteServerStreamEventsAsync(HttpContext httpContext, Stream c
152150
await responseStream.FlushAsync(httpContext.RequestAborted).ConfigureAwait(false);
153151
}
154152
}
155-
156-
async Task ProcessWebSocketRequest(HttpContext httpContext, string endpoint)
157-
{
158-
using var clientWebsocket = await httpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
159-
160-
try
161-
{
162-
await authenticationFilter.ValidateAuthenticationAsync(httpContext.Request.Headers).ConfigureAwait(false);
163-
}
164-
catch (AuthenticationException authenticationException)
165-
{
166-
var message = JsonSerializer.Serialize(new
167-
{
168-
type = "error",
169-
error = new
170-
{
171-
type = "invalid_request_error",
172-
code = "invalid_session_token",
173-
message = authenticationException.Message
174-
}
175-
});
176-
await clientWebsocket.SendAsync(System.Text.Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, httpContext.RequestAborted).ConfigureAwait(false);
177-
await clientWebsocket.CloseAsync(WebSocketCloseStatus.PolicyViolation, authenticationException.Message, httpContext.RequestAborted).ConfigureAwait(false);
178-
return;
179-
}
180-
181-
if (endpoint.EndsWith("echo"))
182-
{
183-
await EchoAsync(clientWebsocket, httpContext.RequestAborted);
184-
return;
185-
}
186-
187-
using var hostWebsocket = new ClientWebSocket();
188-
189-
foreach (var header in client.WebsocketHeaders)
190-
{
191-
hostWebsocket.Options.SetRequestHeader(header.Key, header.Value);
192-
}
193-
194-
var uri = new Uri(string.Format(
195-
client.Settings.BaseWebSocketUrlFormat,
196-
$"{endpoint}{httpContext.Request.QueryString}"
197-
));
198-
await hostWebsocket.ConnectAsync(uri, httpContext.RequestAborted).ConfigureAwait(false);
199-
var receive = ProxyWebSocketMessages(clientWebsocket, hostWebsocket, httpContext.RequestAborted);
200-
var send = ProxyWebSocketMessages(hostWebsocket, clientWebsocket, httpContext.RequestAborted);
201-
await Task.WhenAll(receive, send).ConfigureAwait(false);
202-
return;
203-
204-
async Task ProxyWebSocketMessages(WebSocket fromSocket, WebSocket toSocket, CancellationToken cancellationToken)
205-
{
206-
var buffer = new byte[1024 * 4];
207-
var memoryBuffer = buffer.AsMemory();
208-
209-
while (fromSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
210-
{
211-
var result = await fromSocket.ReceiveAsync(memoryBuffer, cancellationToken).ConfigureAwait(false);
212-
213-
if (fromSocket.CloseStatus.HasValue || result.MessageType == WebSocketMessageType.Close)
214-
{
215-
await toSocket.CloseOutputAsync(fromSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, fromSocket.CloseStatusDescription ?? "Closing", cancellationToken).ConfigureAwait(false);
216-
break;
217-
}
218-
219-
await toSocket.SendAsync(memoryBuffer[..result.Count], result.MessageType, result.EndOfMessage, cancellationToken).ConfigureAwait(false);
220-
}
221-
}
222-
}
223-
224-
static async Task EchoAsync(WebSocket webSocket, CancellationToken cancellationToken)
225-
{
226-
var buffer = new byte[1024 * 4];
227-
var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
228-
229-
while (!receiveResult.CloseStatus.HasValue)
230-
{
231-
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, receiveResult.Count), receiveResult.MessageType, receiveResult.EndOfMessage, cancellationToken);
232-
receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
233-
}
234-
235-
await webSocket.CloseAsync(receiveResult.CloseStatus.Value, receiveResult.CloseStatusDescription, cancellationToken);
236-
}
237153
}
238154
}
239155
}

OpenAI-DotNet-Proxy/Proxy/OpenAIProxy.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
4242
SetupServices(app.ApplicationServices);
4343

4444
app.UseHttpsRedirection();
45-
app.UseWebSockets();
4645
app.UseRouting();
4746
app.UseEndpoints(endpoints =>
4847
{
@@ -82,7 +81,8 @@ public static IHost CreateDefaultHost<T>(string[] args, OpenAIClient openAIClien
8281
/// <typeparam name="T"><see cref="IAuthenticationFilter"/> type to use to validate your custom issued tokens.</typeparam>
8382
/// <param name="args">Startup args.</param>
8483
/// <param name="openAIClient"><see cref="OpenAIClient"/> with configured <see cref="OpenAIAuthentication"/> and <see cref="OpenAISettings"/>.</param>
85-
public static WebApplication CreateWebApplication<T>(string[] args, OpenAIClient openAIClient) where T : class, IAuthenticationFilter
84+
/// <param name="routePrefix"></param>
85+
public static WebApplication CreateWebApplication<T>(string[] args, OpenAIClient openAIClient, string routePrefix = "") where T : class, IAuthenticationFilter
8686
{
8787
var builder = WebApplication.CreateBuilder(args);
8888
builder.Logging.ClearProviders();

OpenAI-DotNet-Tests/AbstractTestFixture.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66
using System;
77
using System.IO;
88
using System.Net.Http;
9-
using System.Net.WebSockets;
10-
using System.Threading;
11-
using System.Threading.Tasks;
129

1310
namespace OpenAI.Tests
1411
{
@@ -42,24 +39,7 @@ protected AbstractTestFixture()
4239
OpenAIClient = new OpenAIClient(auth, settings, HttpClient)
4340
{
4441
EnableDebug = true,
45-
CreateWebsocketAsync = CreateWebsocketAsync
4642
};
47-
48-
return;
49-
50-
async Task<WebSocket> CreateWebsocketAsync(Uri uri, CancellationToken cancellationToken)
51-
{
52-
var websocketClient = webApplicationFactory.Server.CreateWebSocketClient();
53-
websocketClient.ConfigureRequest = request =>
54-
{
55-
foreach (var (key, value) in OpenAIClient.WebsocketHeaders)
56-
{
57-
request.Headers[key] = value;
58-
}
59-
};
60-
var websocket = await websocketClient.ConnectAsync(uri, cancellationToken);
61-
return websocket;
62-
}
6343
}
6444

6545
private static Uri GetBaseAddressFromLaunchSettings()

OpenAI-DotNet-Tests/TestFixture_00_01_Proxy.cs

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
using System;
66
using System.IO;
77
using System.Net;
8-
using System.Net.Http;
9-
using System.Net.WebSockets;
10-
using System.Text;
11-
using System.Threading;
128
using System.Threading.Tasks;
139

1410
namespace OpenAI.Tests
@@ -66,72 +62,5 @@ public async Task Test_02_Client_Authenticated()
6662
Console.WriteLine(model);
6763
}
6864
}
69-
70-
[Test]
71-
public async Task Test_03_Client_Unauthenticated()
72-
{
73-
var settings = new OpenAISettings(domain: HttpClient.BaseAddress?.Authority);
74-
var auth = new OpenAIAuthentication("sess-invalid-token");
75-
var openAIClient = new OpenAIClient(auth, settings, HttpClient);
76-
77-
try
78-
{
79-
await openAIClient.ModelsEndpoint.GetModelsAsync();
80-
}
81-
catch (HttpRequestException httpRequestException)
82-
{
83-
Console.WriteLine(httpRequestException);
84-
// System.Net.Http.HttpRequestException : GetModelsAsync Failed! HTTP status code: Unauthorized | Response body: User is not authorized
85-
Assert.AreEqual(HttpStatusCode.Unauthorized, httpRequestException.StatusCode);
86-
}
87-
catch (Exception e)
88-
{
89-
Console.WriteLine(e);
90-
}
91-
}
92-
93-
[Test]
94-
public async Task Test_04_Client_Websocket_Authentication()
95-
{
96-
try
97-
{
98-
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
99-
var realtimeUri = new Uri(string.Format(OpenAIClient.Settings.BaseWebSocketUrlFormat, "echo"));
100-
Console.WriteLine(realtimeUri);
101-
using var websocket = await OpenAIClient.CreateWebsocketAsync.Invoke(realtimeUri, cts.Token);
102-
103-
if (websocket.State != WebSocketState.Open)
104-
{
105-
throw new Exception($"Failed to open WebSocket connection. Current state: {websocket.State}");
106-
}
107-
108-
var data = new byte[1024];
109-
var buffer = new byte[1024 * 4];
110-
var random = new Random();
111-
random.NextBytes(data);
112-
await websocket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Binary, true, cts.Token);
113-
var receiveResult = await websocket.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
114-
Assert.AreEqual(WebSocketMessageType.Binary, receiveResult.MessageType);
115-
Assert.AreEqual(data.Length, receiveResult.Count);
116-
var receivedData = buffer[..receiveResult.Count];
117-
Assert.AreEqual(data.Length, receivedData.Length);
118-
Assert.AreEqual(data, receivedData);
119-
var message = $"hello world! {DateTime.UtcNow}";
120-
var messageData = Encoding.UTF8.GetBytes(message);
121-
await websocket.SendAsync(new ArraySegment<byte>(messageData), WebSocketMessageType.Text, true, cts.Token);
122-
receiveResult = await websocket.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
123-
Assert.AreEqual(WebSocketMessageType.Text, receiveResult.MessageType);
124-
Assert.AreEqual(messageData.Length, receiveResult.Count);
125-
Assert.AreEqual(messageData, buffer[..receiveResult.Count]);
126-
var decodedMessage = Encoding.UTF8.GetString(buffer, 0, receiveResult.Count);
127-
Assert.AreEqual(message, decodedMessage);
128-
await websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Test completed", cts.Token);
129-
}
130-
catch (Exception e)
131-
{
132-
Console.WriteLine(e);
133-
throw;
134-
}
135-
}
13665
}
13766
}

OpenAI-DotNet/Authentication/OpenAISettings.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ namespace OpenAI
1010
/// </summary>
1111
public sealed class OpenAISettings
1212
{
13-
internal const string WS = "ws://";
1413
internal const string WSS = "wss://";
1514
internal const string Http = "http://";
1615
internal const string Https = "https://";
@@ -74,9 +73,7 @@ public OpenAISettings(string domain, string apiVersion = DefaultOpenAIApiVersion
7473
DeploymentId = string.Empty;
7574
BaseRequest = $"/{ApiVersion}/";
7675
BaseRequestUrlFormat = $"{ResourceName}{BaseRequest}{{0}}";
77-
BaseWebSocketUrlFormat = ResourceName.Contains(Https)
78-
? $"{WSS}{domain}{BaseRequest}{{0}}"
79-
: $"{WS}{domain}{BaseRequest}{{0}}";
76+
BaseWebSocketUrlFormat = $"{WSS}{OpenAIDomain}{BaseRequest}{{0}}";
8077
UseOAuthAuthentication = true;
8178
}
8279

OpenAI-DotNet/Common/OpenAIBaseEndpoint.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,18 @@ public abstract class OpenAIBaseEndpoint
3131
/// </remarks>
3232
protected virtual bool? IsAzureDeployment => null;
3333

34-
/// <summary>
35-
/// Indicates if the endpoint is for a WebSocket.
36-
/// </summary>
37-
protected virtual bool? IsWebSocketEndpoint => null;
38-
3934
/// <summary>
4035
/// Gets the full formatted url for the API endpoint.
4136
/// </summary>
4237
/// <param name="endpoint">The endpoint url.</param>
4338
/// <param name="queryParameters">Optional, parameters to add to the endpoint.</param>
4439
protected string GetUrl(string endpoint = "", Dictionary<string, string> queryParameters = null)
40+
=> GetEndpoint(client.Settings.BaseRequestUrlFormat, endpoint, queryParameters);
41+
42+
protected string GetWebsocketUri(string endpoint = "", Dictionary<string, string> queryParameters = null)
43+
=> GetEndpoint(client.Settings.BaseWebSocketUrlFormat, endpoint, queryParameters);
44+
45+
private string GetEndpoint(string baseUrlFormat, string endpoint = "", Dictionary<string, string> queryParameters = null)
4546
{
4647
string route;
4748

@@ -59,9 +60,6 @@ protected string GetUrl(string endpoint = "", Dictionary<string, string> queryPa
5960
route = $"{Root}{endpoint}";
6061
}
6162

62-
var baseUrlFormat = IsWebSocketEndpoint == true
63-
? client.Settings.BaseWebSocketUrlFormat
64-
: client.Settings.BaseRequestUrlFormat;
6563
var result = string.Format(baseUrlFormat, route);
6664

6765
foreach (var defaultQueryParameter in client.Settings.DefaultQueryParameters)

OpenAI-DotNet/Extensions/VoiceActivityDetectionSettingsConverter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ internal class VoiceActivityDetectionSettingsConverter : JsonConverter<IVoiceAct
1111
{
1212
public override IVoiceActivityDetectionSettings Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
1313
{
14+
if (reader.TokenType == JsonTokenType.Null)
15+
{
16+
return null;
17+
}
18+
1419
var root = JsonDocument.ParseValue(ref reader).RootElement;
1520
var type = root.GetProperty("type").GetString() ?? "disabled";
1621

OpenAI-DotNet/Extensions/WebSocket.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public WebSocket(Uri uri, IReadOnlyDictionary<string, string> requestHeaders = n
3030
Address = uri;
3131
RequestHeaders = requestHeaders ?? new Dictionary<string, string>();
3232
SubProtocols = subProtocols ?? new List<string>();
33-
CreateWebsocketAsync = (_, _) => Task.FromResult<System.Net.WebSockets.WebSocket>(new ClientWebSocket());
3433
RunMessageQueue();
3534
}
3635

@@ -122,9 +121,6 @@ public void Dispose()
122121
public async void Connect()
123122
=> await ConnectAsync().ConfigureAwait(false);
124123

125-
// used for unit testing websocket server
126-
internal Func<Uri, CancellationToken, Task<System.Net.WebSockets.WebSocket>> CreateWebsocketAsync;
127-
128124
public async Task ConnectAsync(CancellationToken cancellationToken = default)
129125
{
130126
try
@@ -141,7 +137,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
141137
_lifetimeCts = new CancellationTokenSource();
142138
using var cts = CancellationTokenSource.CreateLinkedTokenSource(_lifetimeCts.Token, cancellationToken);
143139

144-
_socket = await CreateWebsocketAsync.Invoke(Address, cts.Token).ConfigureAwait(false);
140+
_socket = new ClientWebSocket();
145141

146142
if (_socket is ClientWebSocket clientWebSocket)
147143
{

OpenAI-DotNet/OpenAI-DotNet.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-
2929
<AssemblyOriginatorKeyFile>OpenAI-DotNet.pfx</AssemblyOriginatorKeyFile>
3030
<IncludeSymbols>true</IncludeSymbols>
3131
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
32-
<Version>8.7.4</Version>
32+
<Version>8.8.0</Version>
3333
<PackageReleaseNotes>
34+
Version 8.8.0
35+
- Improved RealtimeSession websocket support for proxies
36+
- Proxy no longer handles the websocket connection directly, but instead initiates the connection to the OpenAI api directly using the ephemeral api key
3437
Version 8.7.4
3538
- Updated proxy support for the OpenAI-DotNet-Proxy package
3639
- Renamed OpenAIAuthentication.LoadFromEnv -&gt; OpenAIAuthentication.LoadFromEnvironment

0 commit comments

Comments
 (0)