Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace OpenAI
{
public sealed class OpenAISettingsInfo : ISettingsInfo
{
internal const string WS = "ws://";
internal const string WSS = "wss://";
internal const string Http = "http://";
internal const string Https = "https://";
Expand Down Expand Up @@ -72,9 +71,7 @@ public OpenAISettingsInfo(string domain, string apiVersion = DefaultOpenAIApiVer
DeploymentId = string.Empty;
BaseRequest = $"/{ApiVersion}/";
BaseRequestUrlFormat = $"{ResourceName}{BaseRequest}{{0}}";
BaseWebSocketUrlFormat = ResourceName.Contains(Https)
? $"{WSS}{domain}{BaseRequest}{{0}}"
: $"{WS}{domain}{BaseRequest}{{0}}";
BaseWebSocketUrlFormat = $"{WSS}{OpenAIDomain}{BaseRequest}{{0}}";
UseOAuthAuthentication = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@ protected OpenAIBaseEndpoint(OpenAIClient client) : base(client) { }
/// </remarks>
protected virtual bool? IsAzureDeployment => null;

/// <summary>
/// Indicates if the endpoint is for a WebSocket.
/// </summary>
protected virtual bool? IsWebSocketEndpoint => null;

/// <summary>
/// Gets the full formatted url for the API endpoint.
/// </summary>
/// <param name="endpoint">The endpoint url.</param>
/// <param name="queryParameters">Optional, parameters to add to the endpoint.</param>
protected override string GetUrl(string endpoint = "", Dictionary<string, string> queryParameters = null)
=> GetEndpoint(client.Settings.Info.BaseRequestUrlFormat, endpoint, queryParameters);

protected string GetWebsocketUri(string endpoint = "", Dictionary<string, string> queryParameters = null)
=> GetEndpoint(client.Settings.Info.BaseWebSocketUrlFormat, endpoint, queryParameters);

private string GetEndpoint(string baseUrlFormat, string endpoint = "", Dictionary<string, string> queryParameters = null)
{
string route;

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

var baseUrlFormat = IsWebSocketEndpoint == true
? client.Settings.Info.BaseWebSocketUrlFormat
: client.Settings.Info.BaseRequestUrlFormat;
var url = string.Format(baseUrlFormat, route);

foreach (var defaultQueryParameter in client.Settings.Info.DefaultQueryParameters)
{
queryParameters ??= new Dictionary<string, string>();
queryParameters ??= new();
queryParameters.Add(defaultQueryParameter.Key, defaultQueryParameter.Value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ internal class VoiceActivityDetectionSettingsConverter : JsonConverter
[Preserve]
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}

var jObject = JObject.Load(reader);
var type = jObject["type"]?.Value<string>() ?? "disabled";

Expand Down
20 changes: 0 additions & 20 deletions OpenAI/Packages/com.openai.unity/Runtime/OpenAIClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
using System.Collections.Generic;
using System.Security.Authentication;
using Utilities.WebRequestRest;
using Utilities.WebSockets;

namespace OpenAI
{
Expand Down Expand Up @@ -237,24 +236,5 @@ protected override void ValidateAuthentication()
public ResponsesEndpoint ResponsesEndpoint { get; }

#endregion Endpoints

internal WebSocket CreateWebSocket(string url)
{
return new WebSocket(url, new Dictionary<string, string>
{
#if !PLATFORM_WEBGL
{ "User-Agent", "OpenAI-DotNet" },
{ "OpenAI-Beta", "realtime=v1" },
{ "Authorization", $"Bearer {Authentication.Info.ApiKey}" }
#endif
}, new List<string>
{
#if PLATFORM_WEBGL // Web browsers do not support headers. https://github.com/openai/openai-realtime-api-beta/blob/339e9553a757ef1cf8c767272fc750c1e62effbb/lib/api.js#L76-L80
"realtime",
$"openai-insecure-api-key.{Authentication.Info.ApiKey}",
"openai-beta.realtime-v1"
#endif
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Newtonsoft.Json;
using System;
using UnityEngine.Scripting;

namespace OpenAI.Realtime
{
[Preserve]
public sealed class ClientSecret
{
[Preserve]
public ClientSecret(int? expiresAfter = null)
{
ExpiresAfter = expiresAfter ?? 600;
}

[Preserve]
[JsonConstructor]
internal ClientSecret(
[JsonProperty("value")] string ephemeralApiKey,
[JsonProperty("expires_at")] int? expiresAtUnixTimeSeconds = null,
[JsonProperty("expires_after")] ExpiresAfter expiresAfter = null)
{
EphemeralApiKey = ephemeralApiKey;
ExpiresAtUnixTimeSeconds = expiresAtUnixTimeSeconds;
ExpiresAfter = expiresAfter;
}

[Preserve]
[JsonProperty("value", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string EphemeralApiKey { get; }

[Preserve]
[JsonProperty("expires_at", DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? ExpiresAtUnixTimeSeconds { get; }

[Preserve]
[JsonIgnore]
public DateTime? ExpiresAt => ExpiresAtUnixTimeSeconds.HasValue
? DateTimeOffset.FromUnixTimeSeconds(ExpiresAtUnixTimeSeconds.Value).UtcDateTime
: null;

[Preserve]
[JsonProperty("expires_after", DefaultValueHandling = DefaultValueHandling.Ignore)]
public ExpiresAfter ExpiresAfter { get; }

[Preserve]
public static implicit operator string(ClientSecret clientSecret) => clientSecret?.EphemeralApiKey;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ public sealed class DisabledVAD : IVoiceActivityDetectionSettings

public bool InterruptResponse => false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Newtonsoft.Json;
using UnityEngine.Scripting;

namespace OpenAI.Realtime
{
[Preserve]
public sealed class ExpiresAfter
{
[Preserve]
public ExpiresAfter(int seconds = 600)
{
Seconds = seconds;
}

[Preserve]
[JsonConstructor]
internal ExpiresAfter(string anchor, int seconds)
{
Anchor = anchor;
Seconds = seconds;
}

[Preserve]
[JsonProperty("anchor")]
public string Anchor { get; } = "created_at";

[Preserve]
[JsonProperty("seconds")]
public int Seconds { get; }

[Preserve]
public static implicit operator ExpiresAfter(int seconds)
=> new(seconds);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Newtonsoft.Json;
using OpenAI.Extensions;
using OpenAI.Models;
using System;
using System.Collections.Generic;
Expand All @@ -8,6 +10,8 @@
using System.Threading.Tasks;
using UnityEngine;
using Utilities.Async;
using Utilities.WebRequestRest;
using Utilities.WebSockets;

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

protected override string Root => "realtime";

protected override bool? IsWebSocketEndpoint => true;

/// <summary>
/// Creates a new realtime session with the provided <see cref="SessionConfiguration"/> options.
/// </summary>
Expand All @@ -39,7 +41,33 @@ public async Task<RealtimeSession> CreateSessionAsync(SessionConfiguration confi
queryParameters["model"] = model;
}

var session = new RealtimeSession(client.CreateWebSocket(GetUrl(queryParameters: queryParameters)), EnableDebug);
var payload = JsonConvert.SerializeObject(configuration, OpenAIClient.JsonSerializationOptions);
var createSessionResponse = await Rest.PostAsync(GetUrl("/sessions"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken);
createSessionResponse.Validate(EnableDebug);
var createSession = createSessionResponse.Deserialize<SessionConfiguration>(client);

if (createSession == null ||
string.IsNullOrWhiteSpace(createSession.ClientSecret?.EphemeralApiKey))
{
throw new InvalidOperationException("Failed to create a session. Ensure the configuration is valid and the API key is set.");
}

var websocket = new WebSocket(GetWebsocketUri(queryParameters: queryParameters), new Dictionary<string, string>
{
#if !PLATFORM_WEBGL
{ "User-Agent", "OpenAI-DotNet" },
{ "OpenAI-Beta", "realtime=v1" },
{ "Authorization", $"Bearer {createSession.ClientSecret!.EphemeralApiKey}" }
#endif
}, new List<string>
{
#if PLATFORM_WEBGL // Web browsers do not support headers. https://github.com/openai/openai-realtime-api-beta/blob/339e9553a757ef1cf8c767272fc750c1e62effbb/lib/api.js#L76-L80
"realtime",
$"openai-insecure-api-key.{createSession.ClientSecret!.EphemeralApiKey}",
"openai-beta.realtime-v1"
#endif
});
var session = new RealtimeSession(websocket, EnableDebug);
var sessionCreatedTcs = new TaskCompletionSource<SessionResponse>();

try
Expand All @@ -49,7 +77,6 @@ public async Task<RealtimeSession> CreateSessionAsync(SessionConfiguration confi
await session.ConnectAsync(cancellationToken).ConfigureAwait(true);
var sessionResponse = await sessionCreatedTcs.Task.WithCancellation(cancellationToken).ConfigureAwait(true);
session.Configuration = sessionResponse.SessionConfiguration;
await session.SendAsync(new UpdateSessionRequest(configuration), cancellationToken: cancellationToken).ConfigureAwait(true);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,33 @@ public SemanticVAD(bool createResponse = true, bool interruptResponse = true, VA
Eagerness = eagerness;
}

[JsonConstructor]
internal SemanticVAD(
[JsonProperty("type")] TurnDetectionType type,
[JsonProperty("create_response")] bool createResponse,
[JsonProperty("interrupt_response")] bool interruptResponse,
[JsonProperty("eagerness")] VAD_Eagerness eagerness)
{
Type = type;
CreateResponse = createResponse;
InterruptResponse = interruptResponse;
Eagerness = eagerness;
}

[Preserve]
[JsonProperty("type", DefaultValueHandling = DefaultValueHandling.Ignore)]
public TurnDetectionType Type { get; } = TurnDetectionType.Semantic_VAD;
public TurnDetectionType Type { get; private set; } = TurnDetectionType.Semantic_VAD;

[Preserve]
[JsonProperty("create_response", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool CreateResponse { get; private set; }

[Preserve]
[JsonProperty("interrupt_response", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool InterruptResponse { get; private set; }

[Preserve]
[JsonProperty("eagerness", DefaultValueHandling = DefaultValueHandling.Ignore)]
public VAD_Eagerness Eagerness { get; private set; }
}
}
}
23 changes: 21 additions & 2 deletions OpenAI/Packages/com.openai.unity/Runtime/Realtime/ServerVAD.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,32 @@ public ServerVAD(
DetectionThreshold = detectionThreshold;
}

[JsonConstructor]
internal ServerVAD(
[JsonProperty("type")] TurnDetectionType type,
[JsonProperty("create_response")] bool createResponse,
[JsonProperty("interrupt_response")] bool interruptResponse,
[JsonProperty("prefix_padding_ms")] int? prefixPadding,
[JsonProperty("silence_duration_ms")] int? silenceDuration,
[JsonProperty("threshold")] float? detectionThreshold)
{
Type = type;
CreateResponse = createResponse;
InterruptResponse = interruptResponse;
PrefixPadding = prefixPadding;
SilenceDuration = silenceDuration;
DetectionThreshold = detectionThreshold;
}

[Preserve]
[JsonProperty("type", DefaultValueHandling = DefaultValueHandling.Ignore)]
public TurnDetectionType Type { get; } = TurnDetectionType.Server_VAD;
public TurnDetectionType Type { get; private set; } = TurnDetectionType.Server_VAD;

[Preserve]
[JsonProperty("create_response", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool CreateResponse { get; private set; }

[Preserve]
[JsonProperty("interrupt_response", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool InterruptResponse { get; private set; }

Expand All @@ -42,4 +61,4 @@ public ServerVAD(
[JsonProperty("threshold", DefaultValueHandling = DefaultValueHandling.Ignore)]
public float? DetectionThreshold { get; private set; }
}
}
}
Loading
Loading