From 78373b0912394b50fee05bc799d1e9b609b989b7 Mon Sep 17 00:00:00 2001 From: Assistant builder Date: Thu, 11 Jun 2026 21:48:21 +0000 Subject: [PATCH] Add OpenAIRealtimeUpstream + RealtimeSessionFactory + DI registrations --- backend/Program.cs | 5 ++ backend/Realtime/OpenAIRealtimeUpstream.cs | 56 ++++++++++++++++++++++ backend/Realtime/RealtimeSessionFactory.cs | 37 ++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 backend/Realtime/OpenAIRealtimeUpstream.cs create mode 100644 backend/Realtime/RealtimeSessionFactory.cs diff --git a/backend/Program.cs b/backend/Program.cs index cae286d..3af97c9 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -35,6 +35,11 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/backend/Realtime/OpenAIRealtimeUpstream.cs b/backend/Realtime/OpenAIRealtimeUpstream.cs new file mode 100644 index 0000000..167bfb5 --- /dev/null +++ b/backend/Realtime/OpenAIRealtimeUpstream.cs @@ -0,0 +1,56 @@ +using System.Net.WebSockets; +using System.Text; +using System.Text.Json.Nodes; + +namespace backend.Realtime; + +public class OpenAIRealtimeUpstream : IRealtimeUpstream +{ + private readonly ClientWebSocket _ws = new(); + + public async Task ConnectAsync(string apiKey, string model, CancellationToken ct) + { + _ws.Options.SetRequestHeader("Authorization", $"Bearer {apiKey}"); + _ws.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1"); + var uri = new Uri($"wss://api.openai.com/v1/realtime?model={Uri.EscapeDataString(model)}"); + await _ws.ConnectAsync(uri, ct); + } + + public async Task SendJsonAsync(JsonObject envelope, CancellationToken ct) + { + var json = envelope.ToJsonString(); + var bytes = Encoding.UTF8.GetBytes(json); + await _ws.SendAsync(bytes, WebSocketMessageType.Text, endOfMessage: true, ct); + } + + public async Task ReceiveJsonAsync(CancellationToken ct) + { + var buffer = new byte[8192]; + using var stream = new MemoryStream(); + while (true) + { + WebSocketReceiveResult result; + try { result = await _ws.ReceiveAsync(buffer, ct); } + catch (WebSocketException) { return null; } + catch (OperationCanceledException) { return null; } + + if (result.MessageType == WebSocketMessageType.Close) return null; + stream.Write(buffer, 0, result.Count); + if (result.EndOfMessage) + { + var json = Encoding.UTF8.GetString(stream.ToArray()); + return JsonNode.Parse(json)?.AsObject(); + } + } + } + + public async ValueTask DisposeAsync() + { + if (_ws.State == WebSocketState.Open) + { + try { await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None); } + catch { } + } + _ws.Dispose(); + } +} diff --git a/backend/Realtime/RealtimeSessionFactory.cs b/backend/Realtime/RealtimeSessionFactory.cs new file mode 100644 index 0000000..2f16eae --- /dev/null +++ b/backend/Realtime/RealtimeSessionFactory.cs @@ -0,0 +1,37 @@ +using backend.Conversations; +using backend.Tools; +using Microsoft.Extensions.Logging; + +namespace backend.Realtime; + +public class RealtimeSessionFactory +{ + protected readonly OpenAIKeyProvider keys; + protected readonly ToolRegistry registry; + protected readonly ConversationLog log; + protected readonly ILoggerFactory loggerFactory; + + public RealtimeSessionFactory( + OpenAIKeyProvider keys, ToolRegistry registry, + ConversationLog log, ILoggerFactory loggerFactory) + { + this.keys = keys; + this.registry = registry; + this.log = log; + this.loggerFactory = loggerFactory; + } + + public virtual async Task CreateAsync( + Guid deviceId, + RealtimeSettings cfg, + ISessionOutput output, + IDeviceChannel channel, + CancellationToken ct) + { + var upstream = new OpenAIRealtimeUpstream(); + await upstream.ConnectAsync(keys.GetRequired(), cfg.Model, ct); + return new RealtimeSession( + deviceId, upstream, output, log, registry, cfg, channel, + loggerFactory.CreateLogger()); + } +}