From a26f918161695c64ef8e60cad1e7b5eba027d56c Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 1 May 2025 12:47:30 -0500 Subject: [PATCH] refine websocket session --- .../Models/Options/ChatSessionOptions.cs | 7 ++ .../Services/WaveStreamChannel.cs | 1 + .../BotSharp.Core.Realtime/Using.cs | 4 + .../Websocket/Chat/BotSharpRealtimeSession.cs | 79 +++++++++++++++++++ .../AsyncWebsocketDataCollectionResult.cs | 10 ++- .../AsyncWebsocketDataResultEnumerator.cs | 15 +++- .../WebsocketPipelineResponse.cs} | 8 +- .../LlmRealtimeSession.cs} | 20 ++--- .../BotSharp.Plugin.ChatHub.csproj | 1 + .../ChatStreamMiddleware.cs | 41 ++++------ src/Plugins/BotSharp.Plugin.ChatHub/Using.cs | 12 ++- .../Realtime/RealTimeCompletionProvider.cs | 15 ++-- src/Plugins/BotSharp.Plugin.OpenAI/Using.cs | 4 + 13 files changed, 160 insertions(+), 57 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Core.Realtime/Models/Options/ChatSessionOptions.cs create mode 100644 src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/BotSharpRealtimeSession.cs rename src/Infrastructure/BotSharp.Core.Realtime/Websocket/{Chat => Common}/AsyncWebsocketDataCollectionResult.cs (70%) rename src/Infrastructure/BotSharp.Core.Realtime/Websocket/{Chat => Common}/AsyncWebsocketDataResultEnumerator.cs (68%) rename src/Infrastructure/BotSharp.Core.Realtime/Websocket/{Chat/AiWebsocketPipelineResponse.cs => Common/WebsocketPipelineResponse.cs} (92%) rename src/Infrastructure/BotSharp.Core.Realtime/Websocket/{Chat/RealtimeChatSession.cs => Llm/LlmRealtimeSession.cs} (79%) diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Models/Options/ChatSessionOptions.cs b/src/Infrastructure/BotSharp.Core.Realtime/Models/Options/ChatSessionOptions.cs new file mode 100644 index 00000000..aeec0b6d --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Realtime/Models/Options/ChatSessionOptions.cs @@ -0,0 +1,7 @@ +namespace BotSharp.Core.Realtime.Models.Options; + +public class ChatSessionOptions +{ + public int? BufferSize { get; set; } + public JsonSerializerOptions? JsonOptions { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs b/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs index 34f82379..fba874ff 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Services/WaveStreamChannel.cs @@ -39,6 +39,7 @@ public class WaveStreamChannel : IStreamChannel var waveFormat = new WaveFormat(24000, 16, 1); // 24000 Hz, 16-bit PCM, Mono _bufferedWaveProvider = new BufferedWaveProvider(waveFormat); _bufferedWaveProvider.BufferDuration = TimeSpan.FromMinutes(10); + //_bufferedWaveProvider.BufferLength = 1024; _bufferedWaveProvider.DiscardOnBufferOverflow = true; _waveOut = new WaveOutEvent() diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Using.cs b/src/Infrastructure/BotSharp.Core.Realtime/Using.cs index 68c2458a..6dce4532 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Using.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Using.cs @@ -15,3 +15,7 @@ global using BotSharp.Abstraction.Agents; global using BotSharp.Abstraction.Routing; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Conversations.Models; + +global using BotSharp.Core.Realtime.Models.Chat; +global using BotSharp.Core.Realtime.Models.Options; +global using BotSharp.Core.Realtime.Websocket.Chat; diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/BotSharpRealtimeSession.cs b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/BotSharpRealtimeSession.cs new file mode 100644 index 00000000..b4b36620 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/BotSharpRealtimeSession.cs @@ -0,0 +1,79 @@ +using BotSharp.Core.Realtime.Websocket.Common; +using System.ClientModel; +using System.Runtime.CompilerServices; + +namespace BotSharp.Core.Realtime.Websocket.Chat; + +public class BotSharpRealtimeSession : IDisposable +{ + private readonly IServiceProvider _services; + private readonly WebSocket _websocket; + private readonly ChatSessionOptions? _sessionOptions; + private readonly object _singleReceiveLock = new(); + private AsyncWebsocketDataCollectionResult _receivedCollectionResult; + + public BotSharpRealtimeSession( + IServiceProvider services, + WebSocket websocket, + ChatSessionOptions? sessionOptions) + { + _services = services; + _websocket = websocket; + _sessionOptions = sessionOptions; + } + + public async IAsyncEnumerable ReceiveUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (ClientResult result in ReceiveInnerUpdatesAsync(cancellationToken)) + { + var update = HandleSessionResult(result); + yield return update; + } + } + + private async IAsyncEnumerable ReceiveInnerUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + lock (_singleReceiveLock) + { + _receivedCollectionResult ??= new(_websocket, _sessionOptions, cancellationToken); + } + + await foreach (var result in _receivedCollectionResult) + { + yield return result; + } + } + + private ChatSessionUpdate HandleSessionResult(ClientResult result) + { + using var response = result.GetRawResponse(); + var bytes = response.Content.ToArray(); + var text = Encoding.UTF8.GetString(bytes, 0, bytes.Length); + return new ChatSessionUpdate + { + RawResponse = text + }; + } + + public async Task SendEvent(string message) + { + if (_websocket.State == WebSocketState.Open) + { + var buffer = Encoding.UTF8.GetBytes(message); + await _websocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + } + + public async Task Disconnect() + { + if (_websocket.State == WebSocketState.Open) + { + await _websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + } + } + + public void Dispose() + { + _websocket.Dispose(); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AsyncWebsocketDataCollectionResult.cs b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/AsyncWebsocketDataCollectionResult.cs similarity index 70% rename from src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AsyncWebsocketDataCollectionResult.cs rename to src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/AsyncWebsocketDataCollectionResult.cs index 946f3990..6fbcdb75 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AsyncWebsocketDataCollectionResult.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/AsyncWebsocketDataCollectionResult.cs @@ -1,17 +1,21 @@ +using BotSharp.Core.Realtime.Models.Options; using System.ClientModel; -namespace BotSharp.Core.Realtime.Websocket.Chat; +namespace BotSharp.Core.Realtime.Websocket.Common; -public class AsyncWebsocketDataCollectionResult : AsyncCollectionResult +internal class AsyncWebsocketDataCollectionResult : AsyncCollectionResult { private readonly WebSocket _webSocket; + private readonly ChatSessionOptions? _sessionOptions; private readonly CancellationToken _cancellationToken; public AsyncWebsocketDataCollectionResult( WebSocket webSocket, + ChatSessionOptions? sessionOptions, CancellationToken cancellationToken) { _webSocket = webSocket; + _sessionOptions = sessionOptions; _cancellationToken = cancellationToken; } @@ -22,7 +26,7 @@ public class AsyncWebsocketDataCollectionResult : AsyncCollectionResult GetRawPagesAsync() { - await using var enumerator = new AsyncWebsocketDataResultEnumerator(_webSocket, _cancellationToken); + await using var enumerator = new AsyncWebsocketDataResultEnumerator(_webSocket, _sessionOptions, _cancellationToken); while (await enumerator.MoveNextAsync().ConfigureAwait(false)) { yield return enumerator.Current; diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AsyncWebsocketDataResultEnumerator.cs b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/AsyncWebsocketDataResultEnumerator.cs similarity index 68% rename from src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AsyncWebsocketDataResultEnumerator.cs rename to src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/AsyncWebsocketDataResultEnumerator.cs index 98492c48..f7871428 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AsyncWebsocketDataResultEnumerator.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/AsyncWebsocketDataResultEnumerator.cs @@ -1,21 +1,28 @@ +using BotSharp.Core.Realtime.Models.Options; using System.Buffers; using System.ClientModel; -namespace BotSharp.Core.Realtime.Websocket.Chat; +namespace BotSharp.Core.Realtime.Websocket.Common; -public class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator +internal class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator { private readonly WebSocket _webSocket; + private readonly ChatSessionOptions? _sessionOptions; private readonly CancellationToken _cancellationToken; private readonly byte[] _buffer; + private const int DEFAULT_BUFFER_SIZE = 1024 * 32; + public AsyncWebsocketDataResultEnumerator( WebSocket webSocket, + ChatSessionOptions? sessionOptions, CancellationToken cancellationToken) { _webSocket = webSocket; + _sessionOptions = sessionOptions; _cancellationToken = cancellationToken; - _buffer = ArrayPool.Shared.Rent(1024 * 32); + var bufferSize = sessionOptions?.BufferSize > 0 ? sessionOptions.BufferSize.Value : DEFAULT_BUFFER_SIZE; + _buffer = ArrayPool.Shared.Rent(bufferSize); } public ClientResult Current { get; private set; } @@ -29,7 +36,7 @@ public class AsyncWebsocketDataResultEnumerator : IAsyncEnumerator public async ValueTask MoveNextAsync() { - var response = new AiWebsocketPipelineResponse(); + var response = new WebsocketPipelineResponse(); while (!response.IsComplete) { var receivedResult = await _webSocket.ReceiveAsync(new(_buffer), _cancellationToken); diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AiWebsocketPipelineResponse.cs b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/WebsocketPipelineResponse.cs similarity index 92% rename from src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AiWebsocketPipelineResponse.cs rename to src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/WebsocketPipelineResponse.cs index fde66e4f..fbbf9ff2 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/AiWebsocketPipelineResponse.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Common/WebsocketPipelineResponse.cs @@ -1,11 +1,11 @@ using System.ClientModel.Primitives; using System.Net; -namespace BotSharp.Core.Realtime.Websocket.Chat; +namespace BotSharp.Core.Realtime.Websocket.Common; -public class AiWebsocketPipelineResponse : PipelineResponse +internal class WebsocketPipelineResponse : PipelineResponse { - public AiWebsocketPipelineResponse() + public WebsocketPipelineResponse() { } @@ -53,7 +53,7 @@ public class AiWebsocketPipelineResponse : PipelineResponse } else if (receivedResult.MessageType != WebSocketMessageType.Text) { - throw new NotImplementedException($"{nameof(AiWebsocketPipelineResponse)} currently supports only text messages."); + throw new NotImplementedException($"{nameof(WebsocketPipelineResponse)} currently supports only text messages."); } var rawBytes = receivedBytes.ToArray(); diff --git a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/RealtimeChatSession.cs b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Llm/LlmRealtimeSession.cs similarity index 79% rename from src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/RealtimeChatSession.cs rename to src/Infrastructure/BotSharp.Core.Realtime/Websocket/Llm/LlmRealtimeSession.cs index 4b1d79ef..4d956df0 100644 --- a/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Chat/RealtimeChatSession.cs +++ b/src/Infrastructure/BotSharp.Core.Realtime/Websocket/Llm/LlmRealtimeSession.cs @@ -1,25 +1,27 @@ using System.ClientModel; using System.Runtime.CompilerServices; using BotSharp.Core.Realtime.Models.Chat; +using BotSharp.Core.Realtime.Models.Options; +using BotSharp.Core.Realtime.Websocket.Common; -namespace BotSharp.Core.Realtime.Websocket.Chat; +namespace BotSharp.Core.Realtime.Websocket.Llm; -public class RealtimeChatSession : IDisposable +public class LlmRealtimeSession : IDisposable { private readonly IServiceProvider _services; - private readonly JsonSerializerOptions _jsonOptions; + private readonly ChatSessionOptions? _sessionOptions; private ClientWebSocket _webSocket; private readonly object _singleReceiveLock = new(); private readonly SemaphoreSlim _clientEventSemaphore = new(initialCount: 1, maxCount: 1); private AsyncWebsocketDataCollectionResult _receivedCollectionResult; - public RealtimeChatSession( + public LlmRealtimeSession( IServiceProvider services, - JsonSerializerOptions jsonOptions) + ChatSessionOptions? sessionOptions = null) { _services = services; - _jsonOptions = jsonOptions; + _sessionOptions = sessionOptions; } public async Task ConnectAsync(Uri uri, Dictionary headers, CancellationToken cancellationToken = default) @@ -44,11 +46,11 @@ public class RealtimeChatSession : IDisposable } } - public async IAsyncEnumerable ReceiveInnerUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + private async IAsyncEnumerable ReceiveInnerUpdatesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { lock (_singleReceiveLock) { - _receivedCollectionResult ??= new(_webSocket, cancellationToken); + _receivedCollectionResult ??= new(_webSocket, _sessionOptions, cancellationToken); } await foreach (var result in _receivedCollectionResult) @@ -81,7 +83,7 @@ public class RealtimeChatSession : IDisposable { if (message is not string data) { - data = JsonSerializer.Serialize(message, _jsonOptions); + data = JsonSerializer.Serialize(message, _sessionOptions?.JsonOptions); } var buffer = Encoding.UTF8.GetBytes(data); diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj index bf8d28a6..1d3e6633 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj +++ b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs index eaecaae9..e9459650 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/ChatStreamMiddleware.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Realtime; -using BotSharp.Abstraction.Realtime.Models; using Microsoft.AspNetCore.Http; using System.Net.WebSockets; @@ -9,6 +7,7 @@ public class ChatStreamMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; + private BotSharpRealtimeSession _session; public ChatStreamMiddleware( RequestDelegate next, @@ -38,6 +37,7 @@ public class ChatStreamMiddleware } catch (Exception ex) { + _session?.Dispose(); _logger.LogError(ex, $"Error when connecting Chat stream. ({ex.Message})"); } return; @@ -49,6 +49,13 @@ public class ChatStreamMiddleware private async Task HandleWebSocket(IServiceProvider services, string agentId, string conversationId, WebSocket webSocket) { + _session?.Dispose(); + _session = new BotSharpRealtimeSession(services, webSocket, new ChatSessionOptions + { + BufferSize = 1024 * 16, + JsonOptions = BotSharpOptions.defaultJsonOptions + }); + var hub = services.GetRequiredService(); var conn = hub.SetHubConnection(conversationId); conn.CurrentAgentId = agentId; @@ -58,26 +65,15 @@ public class ChatStreamMiddleware convService.SetConversationId(conversationId, []); await convService.GetConversationRecordOrCreateNew(agentId); - var buffer = new byte[1024 * 32]; - WebSocketReceiveResult result; - - do + await foreach (ChatSessionUpdate update in _session.ReceiveUpdatesAsync(CancellationToken.None)) { - result = await webSocket.ReceiveAsync(new(buffer), CancellationToken.None); - - if (result.MessageType != WebSocketMessageType.Text) - { - continue; - } - - var receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count); + var receivedText = update?.RawResponse; if (string.IsNullOrEmpty(receivedText)) { continue; } var (eventType, data) = MapEvents(conn, receivedText); - if (eventType == "start") { await ConnectToModel(hub, webSocket); @@ -95,28 +91,19 @@ public class ChatStreamMiddleware break; } } - while (!webSocket.CloseStatus.HasValue); - await webSocket.CloseAsync(result?.CloseStatus ?? WebSocketCloseStatus.NormalClosure, result?.CloseStatusDescription, CancellationToken.None); + _session?.Disconnect(); + _session?.Dispose(); } private async Task ConnectToModel(IRealtimeHub hub, WebSocket webSocket) { await hub.ConnectToModel(async data => { - await SendEventToUser(webSocket, data); + await _session.SendEvent(data); }); } - private async Task SendEventToUser(WebSocket webSocket, string message) - { - if (webSocket.State == WebSocketState.Open) - { - var buffer = Encoding.UTF8.GetBytes(message); - await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); - } - } - private (string, string) MapEvents(RealtimeHubConnection conn, string receivedText) { var response = JsonSerializer.Deserialize(receivedText); diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs b/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs index 73240fbb..8de5816b 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/Using.cs @@ -18,8 +18,6 @@ global using BotSharp.Abstraction.Agents.Settings; global using BotSharp.Abstraction.Conversations.Settings; global using BotSharp.Abstraction.Agents.Enums; global using BotSharp.Abstraction.Conversations.Models; -global using BotSharp.OpenAPI.ViewModels.Conversations; -global using BotSharp.OpenAPI.ViewModels.Users; global using BotSharp.Abstraction.Agents.Models; global using BotSharp.Abstraction.Functions.Models; global using BotSharp.Abstraction.Loggers; @@ -32,6 +30,14 @@ global using BotSharp.Abstraction.Messaging; global using BotSharp.Abstraction.Messaging.Enums; global using BotSharp.Abstraction.Messaging.Models.RichContent; global using BotSharp.Abstraction.Templating; +global using BotSharp.Abstraction.Realtime; +global using BotSharp.Abstraction.Realtime.Models; +global using BotSharp.OpenAPI.ViewModels.Conversations; +global using BotSharp.OpenAPI.ViewModels.Users; global using BotSharp.Plugin.ChatHub.Settings; global using BotSharp.Plugin.ChatHub.Enums; -global using BotSharp.Plugin.ChatHub.Models.Stream; \ No newline at end of file +global using BotSharp.Plugin.ChatHub.Models.Stream; + +global using BotSharp.Core.Realtime.Models.Chat; +global using BotSharp.Core.Realtime.Models.Options; +global using BotSharp.Core.Realtime.Websocket.Chat; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs index 0c8c870c..12618493 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Providers/Realtime/RealTimeCompletionProvider.cs @@ -1,5 +1,3 @@ -using BotSharp.Core.Realtime.Models.Chat; -using BotSharp.Core.Realtime.Websocket.Chat; using BotSharp.Plugin.OpenAI.Models.Realtime; using OpenAI.Chat; @@ -19,7 +17,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion private readonly BotSharpOptions _botsharpOptions; protected string _model = "gpt-4o-mini-realtime-preview"; - private RealtimeChatSession _session; + private LlmRealtimeSession _session; public RealTimeCompletionProvider( RealtimeModelSettings settings, @@ -54,15 +52,18 @@ public class RealTimeCompletionProvider : IRealTimeCompletion { _session.Dispose(); } - _session = new RealtimeChatSession(_services, _botsharpOptions.JsonSerializerOptions); + _session = new LlmRealtimeSession(_services, new ChatSessionOptions + { + JsonOptions = _botsharpOptions.JsonSerializerOptions + }); await _session.ConnectAsync( - new Uri($"wss://api.openai.com/v1/realtime?model={_model}"), - new Dictionary + uri: new Uri($"wss://api.openai.com/v1/realtime?model={_model}"), + headers: new Dictionary { {"Authorization", $"Bearer {settings.ApiKey}"}, {"OpenAI-Beta", "realtime=v1"} }, - CancellationToken.None); + cancellationToken: CancellationToken.None); _ = ReceiveMessage( conn, diff --git a/src/Plugins/BotSharp.Plugin.OpenAI/Using.cs b/src/Plugins/BotSharp.Plugin.OpenAI/Using.cs index 6847cd86..11aa2608 100644 --- a/src/Plugins/BotSharp.Plugin.OpenAI/Using.cs +++ b/src/Plugins/BotSharp.Plugin.OpenAI/Using.cs @@ -32,3 +32,7 @@ global using BotSharp.Abstraction.Realtime.Models; global using BotSharp.Core.Infrastructures; global using BotSharp.Plugin.OpenAI.Models; global using BotSharp.Plugin.OpenAI.Settings; + +global using BotSharp.Core.Realtime.Models.Chat; +global using BotSharp.Core.Realtime.Models.Options; +global using BotSharp.Core.Realtime.Websocket.Llm; \ No newline at end of file