Skip to content

Commit c45a8f5

Browse files
com.openai.unity 8.8.0 (#391)
- 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
1 parent 65eb3e9 commit c45a8f5

File tree

14 files changed

+244
-58
lines changed

14 files changed

+244
-58
lines changed

OpenAI/Packages/com.openai.unity/Runtime/Authentication/OpenAISettingsInfo.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ namespace OpenAI
88
{
99
public sealed class OpenAISettingsInfo : ISettingsInfo
1010
{
11-
internal const string WS = "ws://";
1211
internal const string WSS = "wss://";
1312
internal const string Http = "http://";
1413
internal const string Https = "https://";
@@ -72,9 +71,7 @@ public OpenAISettingsInfo(string domain, string apiVersion = DefaultOpenAIApiVer
7271
DeploymentId = string.Empty;
7372
BaseRequest = $"/{ApiVersion}/";
7473
BaseRequestUrlFormat = $"{ResourceName}{BaseRequest}{{0}}";
75-
BaseWebSocketUrlFormat = ResourceName.Contains(Https)
76-
? $"{WSS}{domain}{BaseRequest}{{0}}"
77-
: $"{WS}{domain}{BaseRequest}{{0}}";
74+
BaseWebSocketUrlFormat = $"{WSS}{OpenAIDomain}{BaseRequest}{{0}}";
7875
UseOAuthAuthentication = true;
7976
}
8077

OpenAI/Packages/com.openai.unity/Runtime/Common/OpenAIBaseEndpoint.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ protected OpenAIBaseEndpoint(OpenAIClient client) : base(client) { }
2121
/// </remarks>
2222
protected virtual bool? IsAzureDeployment => null;
2323

24-
/// <summary>
25-
/// Indicates if the endpoint is for a WebSocket.
26-
/// </summary>
27-
protected virtual bool? IsWebSocketEndpoint => null;
28-
2924
/// <summary>
3025
/// Gets the full formatted url for the API endpoint.
3126
/// </summary>
3227
/// <param name="endpoint">The endpoint url.</param>
3328
/// <param name="queryParameters">Optional, parameters to add to the endpoint.</param>
3429
protected override string GetUrl(string endpoint = "", Dictionary<string, string> queryParameters = null)
30+
=> GetEndpoint(client.Settings.Info.BaseRequestUrlFormat, endpoint, queryParameters);
31+
32+
protected string GetWebsocketUri(string endpoint = "", Dictionary<string, string> queryParameters = null)
33+
=> GetEndpoint(client.Settings.Info.BaseWebSocketUrlFormat, endpoint, queryParameters);
34+
35+
private string GetEndpoint(string baseUrlFormat, string endpoint = "", Dictionary<string, string> queryParameters = null)
3536
{
3637
string route;
3738

@@ -49,14 +50,11 @@ protected override string GetUrl(string endpoint = "", Dictionary<string, string
4950
route = $"{Root}{endpoint}";
5051
}
5152

52-
var baseUrlFormat = IsWebSocketEndpoint == true
53-
? client.Settings.Info.BaseWebSocketUrlFormat
54-
: client.Settings.Info.BaseRequestUrlFormat;
5553
var url = string.Format(baseUrlFormat, route);
5654

5755
foreach (var defaultQueryParameter in client.Settings.Info.DefaultQueryParameters)
5856
{
59-
queryParameters ??= new Dictionary<string, string>();
57+
queryParameters ??= new();
6058
queryParameters.Add(defaultQueryParameter.Key, defaultQueryParameter.Value);
6159
}
6260

OpenAI/Packages/com.openai.unity/Runtime/Extensions/VoiceActivityDetectionSettingsConverter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ internal class VoiceActivityDetectionSettingsConverter : JsonConverter
1717
[Preserve]
1818
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
1919
{
20+
if (reader.TokenType == JsonToken.Null)
21+
{
22+
return null;
23+
}
24+
2025
var jObject = JObject.Load(reader);
2126
var type = jObject["type"]?.Value<string>() ?? "disabled";
2227

OpenAI/Packages/com.openai.unity/Runtime/OpenAIClient.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
using System.Collections.Generic;
2121
using System.Security.Authentication;
2222
using Utilities.WebRequestRest;
23-
using Utilities.WebSockets;
2423

2524
namespace OpenAI
2625
{
@@ -237,24 +236,5 @@ protected override void ValidateAuthentication()
237236
public ResponsesEndpoint ResponsesEndpoint { get; }
238237

239238
#endregion Endpoints
240-
241-
internal WebSocket CreateWebSocket(string url)
242-
{
243-
return new WebSocket(url, new Dictionary<string, string>
244-
{
245-
#if !PLATFORM_WEBGL
246-
{ "User-Agent", "OpenAI-DotNet" },
247-
{ "OpenAI-Beta", "realtime=v1" },
248-
{ "Authorization", $"Bearer {Authentication.Info.ApiKey}" }
249-
#endif
250-
}, new List<string>
251-
{
252-
#if PLATFORM_WEBGL // Web browsers do not support headers. https://github.com/openai/openai-realtime-api-beta/blob/339e9553a757ef1cf8c767272fc750c1e62effbb/lib/api.js#L76-L80
253-
"realtime",
254-
$"openai-insecure-api-key.{Authentication.Info.ApiKey}",
255-
"openai-beta.realtime-v1"
256-
#endif
257-
});
258-
}
259239
}
260240
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Licensed under the MIT License. See LICENSE in the project root for license information.
2+
3+
using Newtonsoft.Json;
4+
using System;
5+
using UnityEngine.Scripting;
6+
7+
namespace OpenAI.Realtime
8+
{
9+
[Preserve]
10+
public sealed class ClientSecret
11+
{
12+
[Preserve]
13+
public ClientSecret(int? expiresAfter = null)
14+
{
15+
ExpiresAfter = expiresAfter ?? 600;
16+
}
17+
18+
[Preserve]
19+
[JsonConstructor]
20+
internal ClientSecret(
21+
[JsonProperty("value")] string ephemeralApiKey,
22+
[JsonProperty("expires_at")] int? expiresAtUnixTimeSeconds = null,
23+
[JsonProperty("expires_after")] ExpiresAfter expiresAfter = null)
24+
{
25+
EphemeralApiKey = ephemeralApiKey;
26+
ExpiresAtUnixTimeSeconds = expiresAtUnixTimeSeconds;
27+
ExpiresAfter = expiresAfter;
28+
}
29+
30+
[Preserve]
31+
[JsonProperty("value", DefaultValueHandling = DefaultValueHandling.Ignore)]
32+
public string EphemeralApiKey { get; }
33+
34+
[Preserve]
35+
[JsonProperty("expires_at", DefaultValueHandling = DefaultValueHandling.Ignore)]
36+
public int? ExpiresAtUnixTimeSeconds { get; }
37+
38+
[Preserve]
39+
[JsonIgnore]
40+
public DateTime? ExpiresAt => ExpiresAtUnixTimeSeconds.HasValue
41+
? DateTimeOffset.FromUnixTimeSeconds(ExpiresAtUnixTimeSeconds.Value).UtcDateTime
42+
: null;
43+
44+
[Preserve]
45+
[JsonProperty("expires_after", DefaultValueHandling = DefaultValueHandling.Ignore)]
46+
public ExpiresAfter ExpiresAfter { get; }
47+
48+
[Preserve]
49+
public static implicit operator string(ClientSecret clientSecret) => clientSecret?.EphemeralApiKey;
50+
}
51+
}

OpenAI/Packages/com.openai.unity/Runtime/Realtime/ClientSecret.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

OpenAI/Packages/com.openai.unity/Runtime/Realtime/DisabledVAD.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ public sealed class DisabledVAD : IVoiceActivityDetectionSettings
1212

1313
public bool InterruptResponse => false;
1414
}
15-
}
15+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed under the MIT License. See LICENSE in the project root for license information.
2+
3+
using Newtonsoft.Json;
4+
using UnityEngine.Scripting;
5+
6+
namespace OpenAI.Realtime
7+
{
8+
[Preserve]
9+
public sealed class ExpiresAfter
10+
{
11+
[Preserve]
12+
public ExpiresAfter(int seconds = 600)
13+
{
14+
Seconds = seconds;
15+
}
16+
17+
[Preserve]
18+
[JsonConstructor]
19+
internal ExpiresAfter(string anchor, int seconds)
20+
{
21+
Anchor = anchor;
22+
Seconds = seconds;
23+
}
24+
25+
[Preserve]
26+
[JsonProperty("anchor")]
27+
public string Anchor { get; } = "created_at";
28+
29+
[Preserve]
30+
[JsonProperty("seconds")]
31+
public int Seconds { get; }
32+
33+
[Preserve]
34+
public static implicit operator ExpiresAfter(int seconds)
35+
=> new(seconds);
36+
}
37+
}

OpenAI/Packages/com.openai.unity/Runtime/Realtime/ExpiresAfter.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

OpenAI/Packages/com.openai.unity/Runtime/Realtime/RealtimeEndpoint.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Licensed under the MIT License. See LICENSE in the project root for license information.
22

3+
using Newtonsoft.Json;
4+
using OpenAI.Extensions;
35
using OpenAI.Models;
46
using System;
57
using System.Collections.Generic;
@@ -8,6 +10,8 @@
810
using System.Threading.Tasks;
911
using UnityEngine;
1012
using Utilities.Async;
13+
using Utilities.WebRequestRest;
14+
using Utilities.WebSockets;
1115

1216
namespace OpenAI.Realtime
1317
{
@@ -17,8 +21,6 @@ public RealtimeEndpoint(OpenAIClient client) : base(client) { }
1721

1822
protected override string Root => "realtime";
1923

20-
protected override bool? IsWebSocketEndpoint => true;
21-
2224
/// <summary>
2325
/// Creates a new realtime session with the provided <see cref="SessionConfiguration"/> options.
2426
/// </summary>
@@ -39,7 +41,33 @@ public async Task<RealtimeSession> CreateSessionAsync(SessionConfiguration confi
3941
queryParameters["model"] = model;
4042
}
4143

42-
var session = new RealtimeSession(client.CreateWebSocket(GetUrl(queryParameters: queryParameters)), EnableDebug);
44+
var payload = JsonConvert.SerializeObject(configuration, OpenAIClient.JsonSerializationOptions);
45+
var createSessionResponse = await Rest.PostAsync(GetUrl("/sessions"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken);
46+
createSessionResponse.Validate(EnableDebug);
47+
var createSession = createSessionResponse.Deserialize<SessionConfiguration>(client);
48+
49+
if (createSession == null ||
50+
string.IsNullOrWhiteSpace(createSession.ClientSecret?.EphemeralApiKey))
51+
{
52+
throw new InvalidOperationException("Failed to create a session. Ensure the configuration is valid and the API key is set.");
53+
}
54+
55+
var websocket = new WebSocket(GetWebsocketUri(queryParameters: queryParameters), new Dictionary<string, string>
56+
{
57+
#if !PLATFORM_WEBGL
58+
{ "User-Agent", "OpenAI-DotNet" },
59+
{ "OpenAI-Beta", "realtime=v1" },
60+
{ "Authorization", $"Bearer {createSession.ClientSecret!.EphemeralApiKey}" }
61+
#endif
62+
}, new List<string>
63+
{
64+
#if PLATFORM_WEBGL // Web browsers do not support headers. https://github.com/openai/openai-realtime-api-beta/blob/339e9553a757ef1cf8c767272fc750c1e62effbb/lib/api.js#L76-L80
65+
"realtime",
66+
$"openai-insecure-api-key.{createSession.ClientSecret!.EphemeralApiKey}",
67+
"openai-beta.realtime-v1"
68+
#endif
69+
});
70+
var session = new RealtimeSession(websocket, EnableDebug);
4371
var sessionCreatedTcs = new TaskCompletionSource<SessionResponse>();
4472

4573
try
@@ -49,7 +77,6 @@ public async Task<RealtimeSession> CreateSessionAsync(SessionConfiguration confi
4977
await session.ConnectAsync(cancellationToken).ConfigureAwait(true);
5078
var sessionResponse = await sessionCreatedTcs.Task.WithCancellation(cancellationToken).ConfigureAwait(true);
5179
session.Configuration = sessionResponse.SessionConfiguration;
52-
await session.SendAsync(new UpdateSessionRequest(configuration), cancellationToken: cancellationToken).ConfigureAwait(true);
5380
}
5481
finally
5582
{

0 commit comments

Comments
 (0)