From fe5f45920c39aedaa933a729f1ce8b02d809ed0a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 29 Oct 2024 15:45:53 -0400 Subject: [PATCH 01/12] Add implementations of Microsoft.Extensions.AI's IChatClient / IEmbeddingGenerator This enables OpenAiService to then be consumed by anyone in the .NET ecosystem written in terms of these exchange types. --- OpenAI.SDK/Managers/OpenAIChatClient.cs | 387 ++++++++++++++++++ .../Managers/OpenAIEmbeddingGenerator.cs | 41 ++ OpenAI.SDK/OpenAI.csproj | 4 + 3 files changed, 432 insertions(+) create mode 100644 OpenAI.SDK/Managers/OpenAIChatClient.cs create mode 100644 OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs diff --git a/OpenAI.SDK/Managers/OpenAIChatClient.cs b/OpenAI.SDK/Managers/OpenAIChatClient.cs new file mode 100644 index 00000000..b298e5e5 --- /dev/null +++ b/OpenAI.SDK/Managers/OpenAIChatClient.cs @@ -0,0 +1,387 @@ +using Microsoft.Extensions.AI; +using OpenAI.ObjectModels; +using OpenAI.ObjectModels.RequestModels; +using OpenAI.ObjectModels.ResponseModels; +using OpenAI.ObjectModels.SharedModels; +using System.Text.Json; + +namespace OpenAI.Managers; + +public partial class OpenAIService : IChatClient +{ + private ChatClientMetadata? _chatMetadata; + + /// + ChatClientMetadata IChatClient.Metadata => _chatMetadata ??= new(nameof(OpenAIService), _httpClient.BaseAddress, this._defaultModelId); + + /// + TService? IChatClient.GetService(object? key) where TService : class => + this as TService; + + /// + void IDisposable.Dispose() { } + + /// + async Task IChatClient.CompleteAsync( + IList chatMessages, ChatOptions? options, CancellationToken cancellationToken) + { + ChatCompletionCreateRequest request = CreateRequest(chatMessages, options); + + var response = await this.ChatCompletion.CreateCompletion(request, options?.ModelId, cancellationToken); + ThrowIfNotSuccessful(response); + + string? finishReason = null; + List responseMessages = []; + foreach (ChatChoiceResponse choice in response.Choices) + { + finishReason ??= choice.FinishReason; + + Microsoft.Extensions.AI.ChatMessage m = new() + { + Role = new(choice.Message.Role), + AuthorName = choice.Message.Name, + RawRepresentation = choice + }; + + PopulateContents(choice.Message, m.Contents); + + if (response.ServiceTier is string serviceTier) + { + (m.AdditionalProperties ??= [])[nameof(response.ServiceTier)] = serviceTier; + } + + if (response.SystemFingerPrint is string fingerprint) + { + (m.AdditionalProperties ??= [])[nameof(response.SystemFingerPrint)] = fingerprint; + } + + responseMessages.Add(m); + } + + return new(responseMessages) + { + CreatedAt = DateTimeOffset.FromUnixTimeSeconds(response.CreatedAt), + CompletionId = response.Id, + FinishReason = finishReason is not null ? new(finishReason) : null, + ModelId = response.Model, + RawRepresentation = response, + Usage = response.Usage is { } usage ? GetUsageDetails(usage) : null, + }; + } + + /// + async IAsyncEnumerable IChatClient.CompleteStreamingAsync( + IList chatMessages, ChatOptions? options, CancellationToken cancellationToken) + { + ChatCompletionCreateRequest request = CreateRequest(chatMessages, options); + + await foreach (var response in this.ChatCompletion.CreateCompletionAsStream(request, options?.ModelId, cancellationToken: cancellationToken)) + { + ThrowIfNotSuccessful(response); + + foreach (ChatChoiceResponse choice in response.Choices) + { + StreamingChatCompletionUpdate update = new() + { + AuthorName = choice.Delta.Name, + CompletionId = response.Id, + CreatedAt = DateTimeOffset.FromUnixTimeSeconds(response.CreatedAt), + FinishReason = choice.FinishReason is not null ? new(choice.FinishReason) : null, + ModelId = response.Model, + RawRepresentation = response, + Role = choice.Delta.Role is not null ? new(choice.Delta.Role) : null, + }; + + if (choice.Index is not null) + { + update.ChoiceIndex = choice.Index.Value; + } + + if (response.ServiceTier is string serviceTier) + { + (update.AdditionalProperties ??= [])[nameof(response.ServiceTier)] = serviceTier; + } + + if (response.SystemFingerPrint is string fingerprint) + { + (update.AdditionalProperties ??= [])[nameof(response.SystemFingerPrint)] = fingerprint; + } + + PopulateContents(choice.Delta, update.Contents); + + yield return update; + + if (response.Usage is { } usage) + { + yield return new() + { + AuthorName = choice.Delta.Name, + CompletionId = response.Id, + Contents = [new UsageContent(GetUsageDetails(usage))], + CreatedAt = DateTimeOffset.FromUnixTimeSeconds(response.CreatedAt), + FinishReason = choice.FinishReason is not null ? new(choice.FinishReason) : null, + ModelId = response.Model, + Role = choice.Delta.Role is not null ? new(choice.Delta.Role) : null, + }; + } + } + } + } + + private static void ThrowIfNotSuccessful(ChatCompletionCreateResponse response) + { + if (!response.Successful) + { + throw new InvalidOperationException(response.Error is { } error ? + $"{response.Error.Code}: {response.Error.Message}" : + "Unknown error"); + } + } + + private ChatCompletionCreateRequest CreateRequest( + IList chatMessages, ChatOptions? options) + { + ChatCompletionCreateRequest request = new() + { + Model = options?.ModelId ?? _defaultModelId + }; + + if (options is not null) + { + // Strongly-typed properties from options + request.MaxCompletionTokens = options.MaxOutputTokens; + request.Temperature = options.Temperature; + request.TopP = options.TopP; + request.FrequencyPenalty = options.FrequencyPenalty; + request.PresencePenalty = options.PresencePenalty; + request.StopAsList = options.StopSequences; + + // Non-strongly-typed properties from additional properties + request.LogitBias = options.AdditionalProperties?.TryGetValue(nameof(request.LogitBias), out object? logitBias) is true ? logitBias : null; + request.LogProbs = options.AdditionalProperties?.TryGetValue(nameof(request.LogProbs), out bool logProbs) is true ? logProbs : null; + request.N = options.AdditionalProperties?.TryGetValue(nameof(request.N), out int n) is true ? n : null; + request.ParallelToolCalls = options.AdditionalProperties?.TryGetValue(nameof(request.ParallelToolCalls), out bool parallelToolCalls) is true ? parallelToolCalls : null; + request.Seed = options.AdditionalProperties?.TryGetValue(nameof(request.Seed), out int seed) is true ? seed : null; + request.ServiceTier = options.AdditionalProperties?.TryGetValue(nameof(request.ServiceTier), out string? serviceTier) is true ? serviceTier : null!; + request.User = options.AdditionalProperties?.TryGetValue(nameof(request.User), out string? user) is true ? user : null!; + request.TopLogprobs = options.AdditionalProperties?.TryGetValue(nameof(request.TopLogprobs), out int topLogprobs) is true ? topLogprobs : null; + + // Response format + switch (options.ResponseFormat) + { + case ChatResponseFormatText: + request.ResponseFormat = new() { Type = StaticValues.CompletionStatics.ResponseFormat.Text }; + break; + + case ChatResponseFormatJson json when json.Schema is not null: + request.ResponseFormat = new() + { + Type = StaticValues.CompletionStatics.ResponseFormat.JsonSchema, + JsonSchema = new JsonSchema() + { + Name = json.SchemaName ?? "JsonSchema", + Schema = JsonSerializer.Deserialize(json.Schema), + Description = json.SchemaDescription, + } + }; + break; + + case ChatResponseFormatJson: + request.ResponseFormat = new() { Type = StaticValues.CompletionStatics.ResponseFormat.Json }; + break; + } + + // Tools + request.Tools = options.Tools?.OfType().Select(f => + { + return ToolDefinition.DefineFunction(new FunctionDefinition() + { + Name = f.Metadata.Name, + Description = f.Metadata.Description, + Parameters = CreateParameters(f) + }); + }).ToList() is { Count: > 0 } tools ? tools : null; + if (request.Tools is not null) + { + request.ToolChoice = + options.ToolMode is RequiredChatToolMode r ? new ToolChoice() + { + Type = StaticValues.CompletionStatics.ToolChoiceType.Required, + Function = r.RequiredFunctionName is null ? null : new ToolChoice.FunctionTool() { Name = r.RequiredFunctionName } + } : + options.ToolMode is AutoChatToolMode ? new ToolChoice() { Type = StaticValues.CompletionStatics.ToolChoiceType.Auto } : + new ToolChoice() { Type = StaticValues.CompletionStatics.ToolChoiceType.None }; + } + } + + // Messages + request.Messages = []; + foreach (var message in chatMessages) + { + foreach (var content in message.Contents) + { + switch (content) + { + case TextContent tc: + request.Messages.Add(new() + { + Content = tc.Text, + Name = message.AuthorName, + Role = message.Role.ToString(), + }); + break; + + case ImageContent ic: + request.Messages.Add(new() + { + Contents = [new() + { + Type = "image_url", + ImageUrl = new MessageImageUrl() + { + Url = ic.Uri, + Detail = ic.AdditionalProperties?.TryGetValue(nameof(MessageImageUrl.Detail), out string? detail) is true ? detail : null, + }, + }], + Name = message.AuthorName, + Role = message.Role.ToString(), + }); + break; + + case FunctionResultContent frc: + request.Messages.Add(new() + { + ToolCallId = frc.CallId, + Content = frc.Result?.ToString(), + Name = message.AuthorName, + Role = message.Role.ToString(), + }); + break; + } + } + + FunctionCallContent[] fccs = message.Contents.OfType().ToArray(); + if (fccs.Length > 0) + { + request.Messages.Add(new() + { + Name = message.AuthorName, + Role = message.Role.ToString(), + ToolCalls = fccs.Select(fcc => new ToolCall() + { + Type = "function", + Id = fcc.CallId, + FunctionCall = new FunctionCall() + { + Name = fcc.Name, + Arguments = JsonSerializer.Serialize(fcc.Arguments) + }, + }).ToList(), + }); + } + } + + return request; + } + + private static PropertyDefinition CreateParameters(AIFunction f) + { + List required = []; + Dictionary properties = []; + + var parameters = f.Metadata.Parameters; + + foreach (AIFunctionParameterMetadata parameter in parameters) + { + properties.Add(parameter.Name, parameter.Schema is JsonElement e ? + e.Deserialize()! : + PropertyDefinition.DefineObject(null, null, null, null, null)); + + if (parameter.IsRequired) + { + required.Add(parameter.Name); + } + } + + return PropertyDefinition.DefineObject(properties, required, null, null, null); + } + + private static void PopulateContents(ObjectModels.RequestModels.ChatMessage source, IList destination) + { + if (source.Content is not null) + { + destination.Add(new TextContent(source.Content)); + } + + if (source.Contents is { } contents) + { + foreach (MessageContent content in contents) + { + if (content.Text is string text) + { + destination.Add(new TextContent(text)); + } + + if (content.ImageUrl is { } url) + { + destination.Add(new ImageContent(url.Url)); + } + } + } + + if (source.ToolCalls is { } toolCalls) + { + foreach (var tc in toolCalls) + { + destination.Add(new FunctionCallContent( + tc.Id ?? string.Empty, + tc.FunctionCall?.Name ?? string.Empty, + tc.FunctionCall?.Arguments is string a ? JsonSerializer.Deserialize>(a) : null)); + } + } + } + + private static UsageDetails GetUsageDetails(UsageResponse usage) + { + var details = new UsageDetails() + { + InputTokenCount = usage.PromptTokens, + OutputTokenCount = usage.CompletionTokens, + TotalTokenCount = usage.TotalTokens, + }; + + if (usage.PromptTokensDetails is { } promptDetails) + { + Dictionary d = new(StringComparer.OrdinalIgnoreCase); + (details.AdditionalProperties ??= [])[nameof(usage.PromptTokensDetails)] = d; + + if (promptDetails.CachedTokens is int cachedTokens) + { + d[nameof(promptDetails.CachedTokens)] = cachedTokens; + } + + if (promptDetails.AudioTokens is int audioTokens) + { + d[nameof(promptDetails.AudioTokens)] = audioTokens; + } + } + + if (usage.CompletionTokensDetails is { } completionDetails) + { + Dictionary d = new(StringComparer.OrdinalIgnoreCase); + (details.AdditionalProperties ??= [])[nameof(usage.CompletionTokensDetails)] = d; + + if (completionDetails.ReasoningTokens is int reasoningTokens) + { + d[nameof(completionDetails.ReasoningTokens)] = reasoningTokens; + } + + if (completionDetails.AudioTokens is int audioTokens) + { + d[nameof(promptDetails.AudioTokens)] = audioTokens; + } + } + + return details; + } +} \ No newline at end of file diff --git a/OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs b/OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs new file mode 100644 index 00000000..455af018 --- /dev/null +++ b/OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.AI; + +namespace OpenAI.Managers; + +public partial class OpenAIService : IEmbeddingGenerator> +{ + private EmbeddingGeneratorMetadata? _embeddingMetadata; + + EmbeddingGeneratorMetadata IEmbeddingGenerator>.Metadata => + _embeddingMetadata ??= new(nameof(OpenAIService), _httpClient.BaseAddress, _defaultModelId); + + TService? IEmbeddingGenerator>.GetService(object? key) where TService : class => + this as TService; + + async Task>> IEmbeddingGenerator>.GenerateAsync(IEnumerable values, EmbeddingGenerationOptions? options, CancellationToken cancellationToken) + { + var response = await this.Embeddings.CreateEmbedding(new() + { + Model = options?.ModelId ?? _defaultModelId, + Dimensions = options?.Dimensions, + InputAsList = values.ToList(), + }, cancellationToken); + + if (!response.Successful) + { + throw new InvalidOperationException(response.Error is { } error ? + $"{response.Error.Code}: {response.Error.Message}" : + "Unknown error"); + } + + return new(response.Data.Select(e => new Embedding(e.Embedding.Select(d => (float)d).ToArray()) { ModelId = response.Model })) + { + Usage = response.Usage is { } usage ? new() + { + InputTokenCount = usage.PromptTokens, + OutputTokenCount = usage.CompletionTokens, + TotalTokenCount = usage.TotalTokens, + } : null, + }; + } +} \ No newline at end of file diff --git a/OpenAI.SDK/OpenAI.csproj b/OpenAI.SDK/OpenAI.csproj index 0dfaaf6e..108227a9 100644 --- a/OpenAI.SDK/OpenAI.csproj +++ b/OpenAI.SDK/OpenAI.csproj @@ -70,4 +70,8 @@ + + + + \ No newline at end of file From 53a8067fc5383358068b3d353e01d03524414eb8 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 1 Nov 2024 09:10:24 -0400 Subject: [PATCH 02/12] Update with renamed namespaces --- OpenAI.SDK/Managers/OpenAIChatClient.cs | 12 ++++++------ OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/OpenAI.SDK/Managers/OpenAIChatClient.cs b/OpenAI.SDK/Managers/OpenAIChatClient.cs index b298e5e5..4e7049a6 100644 --- a/OpenAI.SDK/Managers/OpenAIChatClient.cs +++ b/OpenAI.SDK/Managers/OpenAIChatClient.cs @@ -1,11 +1,11 @@ -using Microsoft.Extensions.AI; -using OpenAI.ObjectModels; -using OpenAI.ObjectModels.RequestModels; -using OpenAI.ObjectModels.ResponseModels; -using OpenAI.ObjectModels.SharedModels; +using Betalgo.Ranul.OpenAI.ObjectModels; +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; +using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; +using Microsoft.Extensions.AI; using System.Text.Json; -namespace OpenAI.Managers; +namespace Betalgo.Ranul.OpenAI.Managers; public partial class OpenAIService : IChatClient { diff --git a/OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs b/OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs index 455af018..42b65899 100644 --- a/OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs +++ b/OpenAI.SDK/Managers/OpenAIEmbeddingGenerator.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.AI; -namespace OpenAI.Managers; +namespace Betalgo.Ranul.OpenAI.Managers; public partial class OpenAIService : IEmbeddingGenerator> { From 1ed80ee565f644ad6f200450af45bf60ad17588f Mon Sep 17 00:00:00 2001 From: Muntadhar Haydar Date: Fri, 1 Nov 2024 15:45:06 +0300 Subject: [PATCH 03/12] adding prop --- .../RequestModels/ChatCompletionCreateRequest.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs index 4763a194..da312664 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs @@ -308,4 +308,12 @@ public IEnumerable Validate() /// [JsonPropertyName("service_tier")] public string? ServiceTier { get; set; } -} \ No newline at end of file + + + /// + /// Whether or not to store the output of this chat completion request for use in our model distillation or evals products. + /// https://platform.openai.com/docs/api-reference/chat/create?lang=python#chat-create-store + /// + [JsonPropertyName("store")] + public bool? Store { get; set; } = false; +} From fd73fde001abd74517ebbac60ee8b477b2f221fd Mon Sep 17 00:00:00 2001 From: Muntadhar Haydar Date: Fri, 1 Nov 2024 18:05:46 +0300 Subject: [PATCH 04/12] adding links --- .../RequestModels/ChatCompletionCreateRequest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs index da312664..af60d842 100644 --- a/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs +++ b/OpenAI.SDK/ObjectModels/RequestModels/ChatCompletionCreateRequest.cs @@ -313,6 +313,11 @@ public IEnumerable Validate() /// /// Whether or not to store the output of this chat completion request for use in our model distillation or evals products. /// https://platform.openai.com/docs/api-reference/chat/create?lang=python#chat-create-store + /// + /// + /// more about distillation: https://platform.openai.com/docs/guides/distillation + /// + /// more about evals: https://platform.openai.com/docs/guides/evals /// [JsonPropertyName("store")] public bool? Store { get; set; } = false; From a765dff5830df77ad1752e23c4da509cb8e73805 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Sun, 10 Nov 2024 20:29:50 +0000 Subject: [PATCH 05/12] Realtime Api Integration --- OpenAI.Playground/OpenAI.Playground.csproj | 10 + OpenAI.Playground/Program.cs | 22 +- OpenAI.Playground/SampleData/Hello.wav | Bin 0 -> 19844 bytes .../SampleData/Tell_Me_Story.wav | Bin 0 -> 169844 bytes .../RealtimeHelpers/RealtimeAudioExample.cs | 199 +++ .../TestHelpers/RealtimeHelpers/VoiceInput.cs | 115 ++ .../RealtimeHelpers/VoiceOutput.cs | 67 + OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj | 4 + .../Builders/OpenAIRealtimeServiceBuilder.cs | 165 +++ ...enAIRealtimeServiceCollectionExtensions.cs | 35 + OpenAI.SDK/Managers/OpenAIRealtimeService.cs | 504 +++++++ .../OpenAIRealtimeServiceClientEvents.cs | 175 +++ .../OpenAIRealtimeServiceServerEvents.cs | 361 +++++ OpenAI.SDK/ObjectModels/Models.cs | 8 +- .../RealtimeModels/RealtimeConstants.cs | 50 + .../RealtimeModels/RealtimeEnums.cs | 197 +++ .../RealtimeModels/RealtimeEventTypes.cs | 119 ++ .../RealtimeModels/RealtimeObjectModels.cs | 1205 +++++++++++++++++ .../RealtimeServiceJsonContext.cs | 76 ++ .../ResponseModels/BaseResponse.cs | 5 + OpenAI.SDK/OpenAiOptions.cs | 5 + OpenAI.sln.DotSettings | 6 +- 22 files changed, 3322 insertions(+), 6 deletions(-) create mode 100644 OpenAI.Playground/SampleData/Hello.wav create mode 100644 OpenAI.Playground/SampleData/Tell_Me_Story.wav create mode 100644 OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs create mode 100644 OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs create mode 100644 OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs create mode 100644 OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs create mode 100644 OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs create mode 100644 OpenAI.SDK/Managers/OpenAIRealtimeService.cs create mode 100644 OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs create mode 100644 OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs create mode 100644 OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs create mode 100644 OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs create mode 100644 OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs create mode 100644 OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeObjectModels.cs create mode 100644 OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeServiceJsonContext.cs diff --git a/OpenAI.Playground/OpenAI.Playground.csproj b/OpenAI.Playground/OpenAI.Playground.csproj index cdcd7b33..858170ab 100644 --- a/OpenAI.Playground/OpenAI.Playground.csproj +++ b/OpenAI.Playground/OpenAI.Playground.csproj @@ -23,9 +23,11 @@ + + @@ -33,6 +35,7 @@ + @@ -40,6 +43,7 @@ + @@ -77,6 +81,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -98,6 +105,9 @@ PreserveNewest + + PreserveNewest + \ No newline at end of file diff --git a/OpenAI.Playground/Program.cs b/OpenAI.Playground/Program.cs index 6eca83d4..97f0ab81 100644 --- a/OpenAI.Playground/Program.cs +++ b/OpenAI.Playground/Program.cs @@ -1,9 +1,16 @@ -using Betalgo.Ranul.OpenAI.Extensions; +using Betalgo.Ranul.OpenAI; +using Betalgo.Ranul.OpenAI.Extensions; using Betalgo.Ranul.OpenAI.Interfaces; +using Betalgo.Ranul.OpenAI.Managers; using LaserCatEyes.HttpClientListener; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using OpenAI.Playground.TestHelpers; +using OpenAI.Playground.TestHelpers.RealtimeHelpers; + +using Microsoft.Extensions.Logging.Console; var builder = new ConfigurationBuilder().AddJsonFile("ApiSettings.json").AddUserSecrets(); @@ -18,6 +25,7 @@ //if you want to use beta services you have to set UseBeta to true. Otherwise, it will use the stable version of OpenAI apis. serviceCollection.AddOpenAIService(r => r.UseBeta = true); +serviceCollection.AddOpenAIRealtimeService(); //serviceCollection.AddOpenAIService(); //// DeploymentId and ResourceName are only for Azure OpenAI. If you want to use Azure OpenAI services you have to set Provider type To Azure. @@ -29,8 +37,12 @@ // options.ResourceName = "MyResourceName"; //}); -var serviceProvider = serviceCollection.BuildServiceProvider(); +var serviceProvider = serviceCollection.AddLogging((loggingBuilder) => loggingBuilder + .SetMinimumLevel(LogLevel.Debug) + .AddConsole() +).BuildServiceProvider(); var sdk = serviceProvider.GetRequiredService(); +var realtimeSdk = serviceProvider.GetRequiredService(); // CHAT GPT // |-----------------------------------------------------------------------| @@ -39,8 +51,10 @@ // | / \ / \ | \ /) | ( \ /o\ / ) | (\ / | / \ / \ | // |-----------------------------------------------------------------------| -await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); -await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); +//await ChatCompletionTestHelper.RunSimpleChatCompletionTest(sdk); +//await ChatCompletionTestHelper.RunSimpleCompletionStreamTest(sdk); + +await (new RealtimeAudioExample(realtimeSdk)).Run(); //Assistants - BETA //await AssistantTestHelper.BasicsTestHelper.RunTests(sdk); diff --git a/OpenAI.Playground/SampleData/Hello.wav b/OpenAI.Playground/SampleData/Hello.wav new file mode 100644 index 0000000000000000000000000000000000000000..12388bc9f1f323a5e3f1bae750986c93adc05481 GIT binary patch literal 19844 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 literal 0 HcmV?d00001 diff --git a/OpenAI.Playground/SampleData/Tell_Me_Story.wav b/OpenAI.Playground/SampleData/Tell_Me_Story.wav new file mode 100644 index 0000000000000000000000000000000000000000..6a60dfd4bff42d3126f800b06b528edec70d756b GIT binary patch literal 169844 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%0A&5erM79>G=Wva9c)`hX| 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

CmG(hT{LzAoGwZ{#u~om7IHxm?N&>21g# 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 + { + { "location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA") }, + { "unit", PropertyDefinition.DefineEnum(["celsius", "fahrenheit"], string.Empty) } + }, ["location"], null, null, null) + } + ] + } + }); + + Console.WriteLine("Press 'R' to start recording, 'S' to stop, 'Q' to quit"); + + while (true) + { + var key = Console.ReadKey(true).Key; + if (key == ConsoleKey.R) + { + _voiceInput.StartRecording(); + Console.WriteLine("Recording started..."); + } + else if (key == ConsoleKey.S) + { + await StopAndSendAudio(); + } + else if (key == ConsoleKey.Q) + { + break; + } + } + } + + private async Task StopAndSendAudio() + { + _voiceInput.StopRecording(); + Console.WriteLine("Recording stopped."); + await _ai.ClientEvents.InputAudioBuffer.Commit(); + await _ai.ClientEvents.Response.Create(); + } + + private async Task SendPreRecordedAudio(string filePath) + { + Console.WriteLine($"Sending pre-recorded audio: {filePath}"); + await _voiceInput.SendAudioFile(filePath); + await _ai.ClientEvents.InputAudioBuffer.Commit(); + } + + private void SetupEventHandlers() + { + // Handle server events related to audio input + _ai.ServerEvents.Conversation.Item.InputAudioTranscription.OnCompleted += (sender, args) => { Console.WriteLine($"Transcription completed: {args.Transcript}"); }; + _ai.ServerEvents.Conversation.Item.InputAudioTranscription.OnFailed += (sender, args) => { Console.WriteLine($"Transcription failed: {args.Error}"); }; + _ai.ServerEvents.InputAudioBuffer.OnCommitted += (sender, args) => { Console.WriteLine("Audio buffer committed."); }; + _ai.ServerEvents.InputAudioBuffer.OnCleared += (sender, args) => { Console.WriteLine("Audio buffer cleared."); }; + _ai.ServerEvents.InputAudioBuffer.OnSpeechStopped += (sender, args) => { Console.WriteLine("Speech stopped detected."); }; + _ai.ServerEvents.InputAudioBuffer.OnSpeechStarted += async (sender, args) => + { + Console.WriteLine("Speech started detected."); + _voiceOutput.StopAndClear(); + + // Optionally, notify the server to cancel any ongoing responses + await _ai.ClientEvents.Response.Cancel(); + }; + _ai.ServerEvents.Response.AudioTranscript.OnDelta += (sender, args) => + { + Console.ForegroundColor = ConsoleColor.DarkGreen; + Console.Write($"{args.Delta}"); + Console.ResetColor(); + }; + _ai.ServerEvents.Response.Audio.OnDelta += (sender, args) => + { + try + { + if (!string.IsNullOrEmpty(args.Delta)) + { + var audioData = Convert.FromBase64String(args.Delta); + _voiceOutput.EnqueueAudioData(audioData); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error processing audio delta: {ex.Message}"); + } + }; + _ai.ServerEvents.Response.Audio.OnDone += (sender, args) => + { + Console.WriteLine(); + Console.WriteLine("Audio response completed."); + }; + + + _ai.ServerEvents.Response.FunctionCallArguments.OnDelta += (sender, args) => + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Function call arguments delta: {args.Delta}"); + Console.ResetColor(); + }; + + _ai.ServerEvents.Response.FunctionCallArguments.OnDone += async (sender, args) => + { + if (args.Arguments != null) + { + Console.WriteLine($"Function call completed: {args.Arguments}"); + if (args.Name == "get_current_weather") + { + await HandleWeatherFunction(args.Arguments, args.CallId); + } + } + }; + _ai.ServerEvents.OnError += (sender, args) => { Console.WriteLine($"Error: {args.Error.Message}"); }; + //for debug + //_ai.ServerEvents.OnAll += (sender, args) => { Console.WriteLine($"Received response: {args}"); }; + } + + private async Task HandleWeatherFunction(string arguments, string callId) + { + try + { + var args = JsonSerializer.Deserialize(arguments); + // Simulate weather API call + var weatherResult = new + { + temperature = args.unit == "celsius" ? 22 : 72, + unit = args.unit, + description = "Sunny with light clouds", + location = args.location + }; + + // Create function output + await _ai.ClientEvents.Conversation.Item.Create(new() + { + Item = new() + { + Type = ItemType.FunctionCallOutput, + CallId = callId, + Output = JsonSerializer.Serialize(weatherResult) + } + }); + + // Generate new response + await _ai.ClientEvents.Response.Create(); + } + catch (Exception ex) + { + Console.WriteLine($"Error handling weather function: {ex.Message}"); + } + } + + private class WeatherArgs + { + public string location { get; set; } + public string unit { get; set; } + } +} \ No newline at end of file diff --git a/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs new file mode 100644 index 00000000..323bb0a1 --- /dev/null +++ b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs @@ -0,0 +1,115 @@ +using Betalgo.Ranul.OpenAI.Managers; +using NAudio.Wave; + +namespace OpenAI.Playground.TestHelpers.RealtimeHelpers; + +public class VoiceInput : IDisposable +{ + private const int MinimumBufferMs = 100; + private readonly List _audioBuffer; + private readonly IOpenAIRealtimeService _client; + private readonly WaveInEvent _waveIn; + private bool _isRecording; + + public VoiceInput(IOpenAIRealtimeService client) + { + _client = client; + _waveIn = new() + { + WaveFormat = new(24000, 16, 1), + BufferMilliseconds = 50 + }; + _audioBuffer = []; + _waveIn.DataAvailable += OnDataAvailable!; + } + + public void Dispose() + { + _waveIn.Dispose(); + } + + public void StartRecording() + { + if (_isRecording) return; + _isRecording = true; + _audioBuffer.Clear(); + _waveIn.StartRecording(); + } + + public void StopRecording() + { + if (!_isRecording) return; + _isRecording = false; + _waveIn.StopRecording(); + + // Send any remaining buffered audio + if (_audioBuffer.Count > 0) + { + _client.ClientEvents.InputAudioBuffer.Append(_audioBuffer.ToArray()); + _audioBuffer.Clear(); + } + } + + private void OnDataAvailable(object sender, WaveInEventArgs e) + { + if (!_isRecording) return; + + // Add new audio data to the buffer + _audioBuffer.AddRange(e.Buffer.Take(e.BytesRecorded)); + + // Calculate buffer duration in milliseconds + var bufferDurationMs = _audioBuffer.Count * 1000.0 / _waveIn.WaveFormat.AverageBytesPerSecond; + + // Only send when we have at least MinimumBufferMs of audio + if (bufferDurationMs >= MinimumBufferMs) + { + _client.ClientEvents.InputAudioBuffer.Append(_audioBuffer.ToArray()); + _audioBuffer.Clear(); + } + } + + public async Task SendAudioFile(string filePath) + { + using var audioFileReader = new AudioFileReader(filePath); + var bufferSize = (int)(audioFileReader.WaveFormat.AverageBytesPerSecond * (MinimumBufferMs / 1000.0)); + var buffer = new byte[bufferSize]; + int bytesRead; + + while ((bytesRead = await audioFileReader.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + if (bytesRead < buffer.Length) + { + // Handle the last partial buffer + var lastBuffer = new byte[bytesRead]; + Array.Copy(buffer, lastBuffer, bytesRead); + buffer = lastBuffer; + } + + var resampledBuffer = ResampleAudio(buffer, bytesRead, audioFileReader.WaveFormat, _waveIn.WaveFormat); + await _client.ClientEvents.InputAudioBuffer.Append(resampledBuffer); + } + } + + private static byte[] ResampleAudio(byte[] buffer, int bytesRead, WaveFormat sourceFormat, WaveFormat targetFormat) + { + if (sourceFormat.SampleRate == targetFormat.SampleRate && sourceFormat.BitsPerSample == targetFormat.BitsPerSample && sourceFormat.Channels == targetFormat.Channels) + { + // No resampling needed + var trimmedBuffer = new byte[bytesRead]; + Array.Copy(buffer, trimmedBuffer, bytesRead); + return trimmedBuffer; + } + + using var sourceStream = new RawSourceWaveStream(buffer, 0, bytesRead, sourceFormat); + using var resampler = new MediaFoundationResampler(sourceStream, targetFormat); + resampler.ResamplerQuality = 60; + + var resampledBytes = (int)(bytesRead * ((double)targetFormat.AverageBytesPerSecond / sourceFormat.AverageBytesPerSecond)); + var resampledBuffer = new byte[resampledBytes]; + var resampledBytesRead = resampler.Read(resampledBuffer, 0, resampledBytes); + + var trimmedBuffer2 = new byte[resampledBytesRead]; + Array.Copy(resampledBuffer, trimmedBuffer2, resampledBytesRead); + return trimmedBuffer2; + } +} \ No newline at end of file diff --git a/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs new file mode 100644 index 00000000..a58fae29 --- /dev/null +++ b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs @@ -0,0 +1,67 @@ +using Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; +using NAudio.Wave; + +namespace OpenAI.Playground.TestHelpers.RealtimeHelpers; + +public class VoiceOutput : IDisposable +{ + private readonly BufferedWaveProvider _bufferedWaveProvider; + private readonly WaveOutEvent _waveOut; + private bool _isPlaying; + + public VoiceOutput() + { + _waveOut = new(); + _waveOut.PlaybackStopped += OnPlaybackStopped!; + _bufferedWaveProvider = new(new(RealtimeConstants.Audio.DefaultSampleRate, RealtimeConstants.Audio.DefaultBitsPerSample, RealtimeConstants.Audio.DefaultChannels)) + { + BufferLength = 10 * 1024 * 1024, // 10 MB buffer - increase if needed + DiscardOnBufferOverflow = true + }; + _waveOut.Init(_bufferedWaveProvider); + } + + public void Dispose() + { + _waveOut.Stop(); + _waveOut.Dispose(); + } + + public void EnqueueAudioData(byte[]? data) + { + if (data == null || data.Length == 0) + return; + + _bufferedWaveProvider.AddSamples(data, 0, data.Length); + + if (!_isPlaying) + { + _waveOut.Play(); + _isPlaying = true; + } + } + + public void StopAndClear() + { + if (_isPlaying) + { + _waveOut.Stop(); + _isPlaying = false; + } + + _bufferedWaveProvider.ClearBuffer(); + Console.WriteLine("Playback stopped and buffer cleared."); + } + + private void OnPlaybackStopped(object sender, StoppedEventArgs e) + { + if (_bufferedWaveProvider.BufferedBytes > 0) + { + _waveOut.Play(); + } + else + { + _isPlaying = false; + } + } +} \ No newline at end of file diff --git a/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj b/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj index 32c37da4..a4b9124e 100644 --- a/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj +++ b/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj @@ -56,19 +56,23 @@ + + + + diff --git a/OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs b/OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs new file mode 100644 index 00000000..d1687a9b --- /dev/null +++ b/OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs @@ -0,0 +1,165 @@ +using System.Net.WebSockets; +using Betalgo.Ranul.OpenAI.Managers; +using Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Betalgo.Ranul.OpenAI.Builders; + +public class OpenAIRealtimeServiceBuilder +{ + private readonly Dictionary _headers = new(); + private readonly OpenAIOptions _options; + private readonly IServiceCollection? _services; + private Action? _configureOptions; + private Action? _configureWebSocket; + private ILogger? _logger; + private ServiceLifetime _serviceLifetime = ServiceLifetime.Singleton; + + // Constructors for standalone and DI scenarios + public OpenAIRealtimeServiceBuilder(string apiKey) : this(new OpenAIOptions { ApiKey = apiKey }) + { + } + + public OpenAIRealtimeServiceBuilder(OpenAIOptions options, ILogger? logger = null) + { + _options = options; + _logger = logger; + } + + public OpenAIRealtimeServiceBuilder(IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + _services = services; + _serviceLifetime = lifetime; + } + + public OpenAIRealtimeServiceBuilder ConfigureWebSocket(Action configure) + { + _configureWebSocket = configure; + return this; + } + + public OpenAIRealtimeServiceBuilder ConfigureOptions(Action configure) + { + _configureOptions = configure; + return this; + } + + public OpenAIRealtimeServiceBuilder AddHeader(string name, string value) + { + _headers[name] = value; + return this; + } + + public OpenAIRealtimeServiceBuilder WithLogger(ILogger logger) + { + _logger = logger; + return this; + } + + public OpenAIRealtimeServiceBuilder AsSingleton() + { + _serviceLifetime = ServiceLifetime.Singleton; + return this; + } + + public OpenAIRealtimeServiceBuilder AsScoped() + { + _serviceLifetime = ServiceLifetime.Scoped; + return this; + } + + public OpenAIRealtimeServiceBuilder AsTransient() + { + _serviceLifetime = ServiceLifetime.Transient; + return this; + } + + public IOpenAIRealtimeService? Build() + { + if (_services == null) + { + // Standalone configuration + var client = new OpenAIWebSocketClient(); + ConfigureClient(client); + return new OpenAIRealtimeService(Options.Create(_options), _logger ?? NullLogger.Instance, client); + } + else + { + switch (_serviceLifetime) + { + case ServiceLifetime.Singleton: + _services.AddSingleton(sp => + { + var client = new OpenAIWebSocketClient(); + ConfigureClient(client); + return client; + }); + _services.AddSingleton(); + break; + case ServiceLifetime.Scoped: + _services.AddScoped(sp => + { + var client = new OpenAIWebSocketClient(); + ConfigureClient(client); + return client; + }); + _services.AddScoped(); + break; + case ServiceLifetime.Transient: + _services.AddTransient(sp => + { + var client = new OpenAIWebSocketClient(); + ConfigureClient(client); + return client; + }); + _services.AddTransient(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + return null; + } + + private void ConfigureClient(OpenAIWebSocketClient client) + { + client.ConfigureWebSocket(ws => WebSocketConfigurationHelper.ConfigureWebSocket(ws, _configureOptions, _headers, _configureWebSocket)); + } +} + +public static class WebSocketConfigurationHelper +{ + public static void ConfigureWebSocket(ClientWebSocket webSocket, Action? configureOptions = null, IReadOnlyDictionary? headers = null, Action? configureWebSocket = null) + { + try + { + webSocket.Options.AddSubProtocol(RealtimeConstants.SubProtocols.Realtime); + webSocket.Options.AddSubProtocol(RealtimeConstants.SubProtocols.Beta); + } + catch (ArgumentException) + { + } + + configureOptions?.Invoke(webSocket.Options); + + if (headers != null) + { + foreach (var header in headers) + { + try + { + webSocket.Options.SetRequestHeader(header.Key, header.Value); + } + catch (ArgumentException) + { + } + } + } + + configureWebSocket?.Invoke(webSocket); + } +} \ No newline at end of file diff --git a/OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs b/OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs new file mode 100644 index 00000000..968b7de1 --- /dev/null +++ b/OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs @@ -0,0 +1,35 @@ +using Betalgo.Ranul.OpenAI.Builders; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Betalgo.Ranul.OpenAI.Extensions; + +public static class OpenAIRealtimeServiceCollectionExtensions +{ + public static OpenAIRealtimeServiceBuilder AddOpenAIRealtimeService(this IServiceCollection services) + { + var optionsBuilder = services.AddOptions(); + optionsBuilder.BindConfiguration(OpenAIOptions.SettingKey); + + var builder = new OpenAIRealtimeServiceBuilder(services); + builder.Build(); + return builder; + } + + public static OpenAIRealtimeServiceBuilder AddOpenAIRealtimeService(this IServiceCollection services, Action configureOptions) + { + services.Configure(configureOptions); + var builder = new OpenAIRealtimeServiceBuilder(services); + builder.Build(); + return builder; + } + + public static OpenAIRealtimeServiceBuilder AddOpenAIRealtimeService(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(OpenAIOptions.SettingKey)); + + var builder = new OpenAIRealtimeServiceBuilder(services); + builder.Build(); + return builder; + } +} \ No newline at end of file diff --git a/OpenAI.SDK/Managers/OpenAIRealtimeService.cs b/OpenAI.SDK/Managers/OpenAIRealtimeService.cs new file mode 100644 index 00000000..b47b8fa6 --- /dev/null +++ b/OpenAI.SDK/Managers/OpenAIRealtimeService.cs @@ -0,0 +1,504 @@ +using System.Buffers; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Betalgo.Ranul.OpenAI.Builders; +using Betalgo.Ranul.OpenAI.ObjectModels; +using Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +#if !NET6_0_OR_GREATER +using System.Text.Json.Serialization; +#endif + +namespace Betalgo.Ranul.OpenAI.Managers; + +public class OpenAIWebSocketClient +{ + public ClientWebSocket WebSocket { get; } = new(); + + public void ConfigureWebSocket(Action configure) + { + configure(WebSocket); + } +} + +public partial class OpenAIRealtimeService : IOpenAIRealtimeService +{ + public OpenAIRealtimeService(IOptions settings, ILogger logger, OpenAIWebSocketClient webSocketClient) + { + _openAIOptions = settings.Value; + _logger = logger; + _webSocketClient = webSocketClient; + _clientEvents = new(this); + _serverEvents = new(); + ConfigureBaseWebSocket(); + } + + public OpenAIRealtimeService(OpenAIOptions options) : this(Options.Create(options), NullLogger.Instance, new()) + { + } + + public OpenAIRealtimeService(OpenAIOptions options, ILogger logger) : this(Options.Create(options), logger, new()) + { + } + + public OpenAIRealtimeService(OpenAIOptions options, Action? configureWebSocket = null) : this(options) + { + if (configureWebSocket != null) + { + _webSocketClient.ConfigureWebSocket(configureWebSocket); + } + } + + public OpenAIRealtimeService(string apiKey) : this(new OpenAIOptions { ApiKey = apiKey }) + { + } + + public static OpenAIRealtimeServiceBuilder Create(string apiKey) + { + return new(apiKey); + } + + public static OpenAIRealtimeServiceBuilder Create(OpenAIOptions options) + { + return new(options); + } + + + private void ConfigureBaseWebSocket() + { + var headers = new Dictionary + { + { RealtimeConstants.Headers.Authorization, $"Bearer {_openAIOptions.ApiKey}" } + }; + + if (!string.IsNullOrEmpty(_openAIOptions.Organization)) + { + headers[RealtimeConstants.Headers.OpenAIOrganization] = _openAIOptions.Organization; + } + + _webSocketClient.ConfigureWebSocket(ws => WebSocketConfigurationHelper.ConfigureWebSocket(ws, headers: headers)); + } +} + +public partial class OpenAIRealtimeService : IOpenAIRealtimeService +{ +#if !NET6_0_OR_GREATER + private static readonly JsonSerializerOptions JsonOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReadCommentHandling = JsonCommentHandling.Disallow, // Disallow comments + AllowTrailingCommas = false, // Disallow trailing commas + PropertyNameCaseInsensitive = false, // Case-sensitive property names + WriteIndented = false // Disable indentation for compact output + }; +#endif + + private readonly CancellationTokenSource _disposeCts = new(); + private readonly ILogger _logger; + private readonly OpenAIOptions _openAIOptions; + private readonly SemaphoreSlim _sendLock = new(1, 1); + private Task? _receiveTask; + private ClientWebSocket? _webSocket; + private readonly object _webSocketLock = new(); + + private readonly ClientEventsImplementation _clientEvents; + private readonly OpenAIRealtimeServiceServerEvents _serverEvents; + private readonly OpenAIWebSocketClient _webSocketClient; + + + private static readonly Dictionary> EventHandlers = InitializeEventHandlers(); + + /// + public bool IsConnected + { + get + { + lock (_webSocketLock) + { + return _webSocket?.State == WebSocketState.Open; + } + } + } + + private static Dictionary> InitializeEventHandlers() + { + return new() + { + // Error + { RealtimeEventTypes.Server.Error, (service, element) => service._serverEvents.RaiseOnError(service, service.DeserializeEvent(element)) }, + + // Session events + { RealtimeEventTypes.Server.Session.Created, (service, element) => service._serverEvents.SessionImpl.RaiseOnCreated(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Session.Updated, (service, element) => service._serverEvents.SessionImpl.RaiseOnUpdated(service, service.DeserializeEvent(element)) }, + + // Conversation events + { RealtimeEventTypes.Server.Conversation.Created, (service, element) => service._serverEvents.ConversationImpl.RaiseOnCreated(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Conversation.Item.Created, (service, element) => service._serverEvents.ConversationImpl.ItemImpl.RaiseOnCreated(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Conversation.Item.Truncated, (service, element) => service._serverEvents.ConversationImpl.ItemImpl.RaiseOnTruncated(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Conversation.Item.Deleted, (service, element) => service._serverEvents.ConversationImpl.ItemImpl.RaiseOnDeleted(service, service.DeserializeEvent(element)) }, + { + RealtimeEventTypes.Server.Conversation.Item.InputAudioTranscription.Completed, + (service, element) => service._serverEvents.ConversationImpl.ItemImpl.InputAudioTranscriptionImpl.RaiseOnCompleted(service, service.DeserializeEvent(element)) + }, + { + RealtimeEventTypes.Server.Conversation.Item.InputAudioTranscription.Failed, + (service, element) => service._serverEvents.ConversationImpl.ItemImpl.InputAudioTranscriptionImpl.RaiseOnFailed(service, service.DeserializeEvent(element)) + }, + + // InputAudioBuffer events + { RealtimeEventTypes.Server.InputAudioBuffer.Committed, (service, element) => service._serverEvents.InputAudioBufferImpl.RaiseOnCommitted(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.InputAudioBuffer.Cleared, (service, element) => service._serverEvents.InputAudioBufferImpl.RaiseOnCleared(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.InputAudioBuffer.SpeechStarted, (service, element) => service._serverEvents.InputAudioBufferImpl.RaiseOnSpeechStarted(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.InputAudioBuffer.SpeechStopped, (service, element) => service._serverEvents.InputAudioBufferImpl.RaiseOnSpeechStopped(service, service.DeserializeEvent(element)) }, + + // Response events + { RealtimeEventTypes.Server.Response.Created, (service, element) => service._serverEvents.ResponseImpl.RaiseOnCreated(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Response.Done, (service, element) => service._serverEvents.ResponseImpl.RaiseOnDone(service, service.DeserializeEvent(element)) }, + + // Response OutputItem events + { RealtimeEventTypes.Server.Response.OutputItem.Added, (service, element) => service._serverEvents.ResponseImpl.OutputItemImpl.RaiseOnAdded(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Response.OutputItem.Done, (service, element) => service._serverEvents.ResponseImpl.OutputItemImpl.RaiseOnDone(service, service.DeserializeEvent(element)) }, + + // Response ContentPart events + { RealtimeEventTypes.Server.Response.ContentPart.Added, (service, element) => service._serverEvents.ResponseImpl.ContentPartImpl.RaiseOnAdded(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Response.ContentPart.Done, (service, element) => service._serverEvents.ResponseImpl.ContentPartImpl.RaiseOnDone(service, service.DeserializeEvent(element)) }, + + // Response Text events + { RealtimeEventTypes.Server.Response.Text.Delta, (service, element) => service._serverEvents.ResponseImpl.TextImpl.RaiseOnDelta(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Response.Text.Done, (service, element) => service._serverEvents.ResponseImpl.TextImpl.RaiseOnDone(service, service.DeserializeEvent(element)) }, + + // Response AudioTranscript events + { RealtimeEventTypes.Server.Response.AudioTranscript.Delta, (service, element) => service._serverEvents.ResponseImpl.AudioTranscriptImpl.RaiseOnDelta(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Response.AudioTranscript.Done, (service, element) => service._serverEvents.ResponseImpl.AudioTranscriptImpl.RaiseOnDone(service, service.DeserializeEvent(element)) }, + + // Response Audio events + { RealtimeEventTypes.Server.Response.Audio.Delta, (service, element) => service._serverEvents.ResponseImpl.AudioImpl.RaiseOnDelta(service, service.DeserializeEvent(element)) }, + { RealtimeEventTypes.Server.Response.Audio.Done, (service, element) => service._serverEvents.ResponseImpl.AudioImpl.RaiseOnDone(service, service.DeserializeEvent(element)) }, + + // Response FunctionCallArguments events + { + RealtimeEventTypes.Server.Response.FunctionCallArguments.Delta, + (service, element) => service._serverEvents.ResponseImpl.FunctionCallArgumentsImpl.RaiseOnDelta(service, service.DeserializeEvent(element)) + }, + { + RealtimeEventTypes.Server.Response.FunctionCallArguments.Done, + (service, element) => service._serverEvents.ResponseImpl.FunctionCallArgumentsImpl.RaiseOnDone(service, service.DeserializeEvent(element)) + }, + + // RateLimits events + { RealtimeEventTypes.Server.RateLimits.Updated, (service, element) => service._serverEvents.RateLimitsImpl.RaiseOnUpdated(service, service.DeserializeEvent(element)) } + }; + } + + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposeCts.Token); + linkedCts.Token.ThrowIfCancellationRequested(); + + if (IsConnected) + throw new InvalidOperationException("Already connected to Realtime API."); + + try + { + var webSocket = _webSocketClient.WebSocket; + var url = new Uri($"{_openAIOptions.BaseRealTimeSocketUrl}?model={_openAIOptions.DefaultModelId ?? Models.Gpt_4o_realtime_preview_2024_10_01}"); + await webSocket.ConnectAsync(url, linkedCts.Token).ConfigureAwait(false); + + if (webSocket.State != WebSocketState.Open) + { + throw new InvalidOperationException($"WebSocket connection failed. Current state: {webSocket.State}"); + } + + lock (_webSocketLock) + { + _webSocket = webSocket; + } + + _logger.LogInformation("Successfully connected to Realtime API"); + _receiveTask = StartReceiving(linkedCts.Token); + } + catch (OperationCanceledException) + { + _logger.LogInformation("Connection canceled."); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to connect to Realtime API"); + throw; + } + } + + /// + public async Task DisconnectAsync(CancellationToken cancellationToken = default) + { + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _disposeCts.Token); + linkedCts.Token.ThrowIfCancellationRequested(); + + ClientWebSocket? webSocket; + lock (_webSocketLock) + { + webSocket = _webSocket; + } + + if (webSocket == null || webSocket.State != WebSocketState.Open) return; + + try + { + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", linkedCts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + _logger.LogInformation("Disconnection canceled."); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during disconnection"); + throw; + } + finally + { + CleanupWebSocket(); + } + } + + private Task HandleMessage(string message) + { + _serverEvents.RaiseOnAll(this, message); + + try + { + using var doc = JsonDocument.Parse(message); + var rootElement = doc.RootElement; + + if (!rootElement.TryGetProperty("type", out var typeElement)) + { + _logger.LogWarning("Received message without type"); + return Task.CompletedTask; + } + + var type = typeElement.GetString(); + + if (type != null && EventHandlers.TryGetValue(type, out var handler)) + { + handler(this, rootElement); + } + else + { + _logger.LogWarning("Received unknown event type: {Type}", type); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing message"); + var errorEvent = new ErrorEvent + { + Error = new() + { + Type = "Ranul.OpenAI_processing_error", + MessageObject = ex.Message + } + }; + _serverEvents.RaiseOnError(this, errorEvent); + } + + return Task.CompletedTask; + } + + private async Task SendEvent(T message, CancellationToken cancellationToken) where T : class + { + try + { + await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { +#if NET6_0_OR_GREATER + var json = JsonSerializer.Serialize(message, message.GetType(), RealtimeServiceJsonContext.Default); +#else + var json = JsonSerializer.Serialize(message, JsonOptions); +#endif + var buffer = Encoding.UTF8.GetBytes(json); + + ClientWebSocket? webSocket; + lock (_webSocketLock) + { + webSocket = _webSocket; + } + + if (webSocket == null) + throw new InvalidOperationException("WebSocket is not initialized."); + + + await webSocket.SendAsync(new(buffer), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); + } + finally + { + _sendLock.Release(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending message"); + throw; + } + } + + private T DeserializeEvent(JsonElement jsonElement) where T : class + { +#if NET6_0_OR_GREATER + var typeInfo = RealtimeServiceJsonContext.Default.GetTypeInfo(typeof(T)); + return typeInfo != null ? jsonElement.Deserialize((JsonTypeInfo)typeInfo)! : jsonElement.Deserialize()!; +#else + // For earlier versions, JsonSerializer does not support deserializing directly from JsonElement. + // We need to use GetRawText(), which does incur some overhead but avoids reparsing the entire message. + return JsonSerializer.Deserialize(jsonElement.GetRawText(), JsonOptions)!; +#endif + } + + private async Task StartReceiving(CancellationToken cancellationToken) + { + ClientWebSocket? webSocket; + lock (_webSocketLock) + { + webSocket = _webSocket; + } + + if (webSocket == null) + throw new InvalidOperationException("WebSocket is not initialized."); + + var buffer = ArrayPool.Shared.Rent(16 * 1); + var textBuffer = new StringBuilder(); + + try + { + while (!cancellationToken.IsCancellationRequested) + { + WebSocketReceiveResult result; + try + { + result = await webSocket.ReceiveAsync(new(buffer), cancellationToken).ConfigureAwait(false); + } + catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) + { + _logger.LogWarning("WebSocket connection closed prematurely."); + break; + } + catch (ObjectDisposedException) + { + break; + } + + if (result.MessageType == WebSocketMessageType.Close) + { + await HandleCloseMessage().ConfigureAwait(false); + break; + } + + if (result.MessageType == WebSocketMessageType.Text) + { + textBuffer.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); + + if (result.EndOfMessage) + { + await HandleMessage(textBuffer.ToString()).ConfigureAwait(false); + textBuffer.Clear(); + } + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Normal cancellation, do nothing + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in receive loop"); + _serverEvents.RaiseOnError(this, new() + { + Error = new() + { + MessageObject = ex.Message, + Type = "Ranul.OpenAI_receive_error" + } + }); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private async Task HandleCloseMessage() + { + await DisconnectAsync().ConfigureAwait(false); + } + + private void CleanupWebSocket() + { + ClientWebSocket? webSocket; + lock (_webSocketLock) + { + webSocket = _webSocket; + _webSocket = null; + } + + if (webSocket != null) + { + if (webSocket.State == WebSocketState.Open) + { + try + { + webSocket.Abort(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error aborting WebSocket"); + } + } + + webSocket.Dispose(); + } + } + + /// + public void Dispose() + { + _disposeCts.Cancel(); + CleanupWebSocket(); + _sendLock.Dispose(); + _disposeCts.Dispose(); + } + + /// + public async ValueTask DisposeAsync() + { + _disposeCts.Cancel(); + + if (_receiveTask != null) + { + await _receiveTask.ConfigureAwait(false); + } + + CleanupWebSocket(); + _sendLock.Dispose(); + _disposeCts.Dispose(); + } +} + +public interface IOpenAIRealtimeService : IDisposable, IAsyncDisposable +{ + bool IsConnected { get; } + + IOpenAIRealtimeServiceClientEvents ClientEvents { get; } + + IOpenAIRealtimeServiceServerEvents ServerEvents { get; } + + Task ConnectAsync(CancellationToken cancellationToken = default); + + Task DisconnectAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs b/OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs new file mode 100644 index 00000000..b6c284f5 --- /dev/null +++ b/OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs @@ -0,0 +1,175 @@ +using Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + +namespace Betalgo.Ranul.OpenAI.Managers; + +public partial class OpenAIRealtimeService +{ + public IOpenAIRealtimeServiceClientEvents ClientEvents => _clientEvents; + + private class ClientEventsImplementation(OpenAIRealtimeService client) : IOpenAIRealtimeServiceClientEvents + { + private readonly ConversationImplementation _conversation = new(client); + private readonly InputAudioBufferImplementation _inputAudioBuffer = new(client); + private readonly ResponseImplementation _response = new(client); + private readonly SessionImplementation _session = new(client); + + public IOpenAIRealtimeServiceClientEvents.ISession Session => _session; + public IOpenAIRealtimeServiceClientEvents.IInputAudioBuffer InputAudioBuffer => _inputAudioBuffer; + public IOpenAIRealtimeServiceClientEvents.IConversation Conversation => _conversation; + public IOpenAIRealtimeServiceClientEvents.IResponse Response => _response; + } +} + +public partial class OpenAIRealtimeService +{ + public IOpenAIRealtimeServiceServerEvents ServerEvents => _serverEvents; + + private class SessionImplementation(OpenAIRealtimeService client) : IOpenAIRealtimeServiceClientEvents.ISession + { + public Task Update(SessionUpdateRequest request, CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + return client.SendEvent(request, cancellationToken); + } + } + + private class InputAudioBufferImplementation(OpenAIRealtimeService client) : IOpenAIRealtimeServiceClientEvents.IInputAudioBuffer + { + public Task Append(ReadOnlyMemory audioData, CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + var request = new AudioBufferAppendRequest + { + Audio = Convert.ToBase64String(audioData.ToArray()) + }; + return client.SendEvent(request, cancellationToken); + } + + public Task Commit(CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + return client.SendEvent(new AudioBufferCommitRequest(), cancellationToken); + } + + public Task Clear(CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + return client.SendEvent(new AudioBufferClearRequest(), cancellationToken); + } + } + + private class ConversationImplementation(OpenAIRealtimeService client) : IOpenAIRealtimeServiceClientEvents.IConversation + { + private readonly ConversationItemImplementation _item = new(client); + + public IOpenAIRealtimeServiceClientEvents.IConversation.IItem Item => _item; + + private class ConversationItemImplementation(OpenAIRealtimeService client) : IOpenAIRealtimeServiceClientEvents.IConversation.IItem + { + public Task Create(ConversationItemCreateRequest request, CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + return client.SendEvent(request, cancellationToken); + } + + public Task Truncate(string itemId, int contentIndex, int audioEndMs, CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + var request = new ConversationItemTruncateRequest + { + ItemId = itemId, + ContentIndex = contentIndex, + AudioEndMs = audioEndMs + }; + return client.SendEvent(request, cancellationToken); + } + + public Task Delete(string itemId, CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + var request = new ConversationItemDeleteRequest + { + ItemId = itemId + }; + return client.SendEvent(request, cancellationToken); + } + } + } + + private class ResponseImplementation(OpenAIRealtimeService client) : IOpenAIRealtimeServiceClientEvents.IResponse + { + public Task Create(CancellationToken cancellationToken = default) + { + return Create(new(), cancellationToken); + } + + public Task Create(ResponseCreateRequest request, CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + return client.SendEvent(request, cancellationToken); + } + + public Task Cancel(CancellationToken cancellationToken = default) + { + if (!client.IsConnected) + throw new InvalidOperationException("Not connected to Realtime API."); + + return client.SendEvent(new ResponseCancelRequest(), cancellationToken); + } + } +} + +public interface IOpenAIRealtimeServiceClientEvents +{ + ISession Session { get; } + IInputAudioBuffer InputAudioBuffer { get; } + IConversation Conversation { get; } + IResponse Response { get; } + + public interface ISession + { + Task Update(SessionUpdateRequest request, CancellationToken cancellationToken = default); + } + + public interface IInputAudioBuffer + { + Task Append(ReadOnlyMemory audioData, CancellationToken cancellationToken = default); + Task Commit(CancellationToken cancellationToken = default); + Task Clear(CancellationToken cancellationToken = default); + } + + public interface IConversation + { + IItem Item { get; } + + public interface IItem + { + Task Create(ConversationItemCreateRequest request, CancellationToken cancellationToken = default); + Task Truncate(string itemId, int contentIndex, int audioEndMs, CancellationToken cancellationToken = default); + Task Delete(string itemId, CancellationToken cancellationToken = default); + } + } + + public interface IResponse + { + Task Create(ResponseCreateRequest request, CancellationToken cancellationToken = default); + Task Create(CancellationToken cancellationToken = default); + Task Cancel(CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs b/OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs new file mode 100644 index 00000000..1597b541 --- /dev/null +++ b/OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs @@ -0,0 +1,361 @@ +using Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + +namespace Betalgo.Ranul.OpenAI.Managers; + +public interface IOpenAIRealtimeServiceServerEvents +{ + ISession Session { get; } + IConversation Conversation { get; } + IInputAudioBuffer InputAudioBuffer { get; } + IResponse Response { get; } + IRateLimits RateLimits { get; } + event EventHandler? OnError; + event EventHandler? OnAll; + + public interface ISession + { + event EventHandler? OnCreated; + event EventHandler? OnUpdated; + } + + public interface IConversation + { + IItem Item { get; } + event EventHandler? OnCreated; + + public interface IItem + { + IInputAudioTranscription InputAudioTranscription { get; } + event EventHandler? OnCreated; + event EventHandler? OnTruncated; + event EventHandler? OnDeleted; + + public interface IInputAudioTranscription + { + event EventHandler? OnCompleted; + event EventHandler? OnFailed; + } + } + } + + public interface IInputAudioBuffer + { + event EventHandler? OnCommitted; + event EventHandler? OnCleared; + event EventHandler? OnSpeechStarted; + event EventHandler? OnSpeechStopped; + } + + public interface IResponse + { + IOutputItem OutputItem { get; } + IContentPart ContentPart { get; } + IText Text { get; } + IAudioTranscript AudioTranscript { get; } + IAudio Audio { get; } + IFunctionCallArguments FunctionCallArguments { get; } + event EventHandler? OnCreated; + event EventHandler? OnDone; + + public interface IOutputItem + { + event EventHandler? OnAdded; + event EventHandler? OnDone; + } + + public interface IContentPart + { + event EventHandler? OnAdded; + event EventHandler? OnDone; + } + + public interface IText + { + event EventHandler? OnDelta; + event EventHandler? OnDone; + } + + public interface IAudioTranscript + { + event EventHandler? OnDelta; + event EventHandler? OnDone; + } + + public interface IAudio + { + event EventHandler? OnDelta; + event EventHandler? OnDone; + } + + public interface IFunctionCallArguments + { + event EventHandler? OnDelta; + event EventHandler? OnDone; + } + } + + public interface IRateLimits + { + event EventHandler? OnUpdated; + } +} + +public class OpenAIRealtimeServiceServerEvents : IOpenAIRealtimeServiceServerEvents +{ + internal readonly ConversationImplementation ConversationImpl = new(); + internal readonly InputAudioBufferImplementation InputAudioBufferImpl = new(); + internal readonly RateLimitsImplementation RateLimitsImpl = new(); + internal readonly ResponseImplementation ResponseImpl = new(); + internal readonly SessionImplementation SessionImpl = new(); + + public event EventHandler? OnError; + public event EventHandler? OnAll; + + public IOpenAIRealtimeServiceServerEvents.ISession Session => SessionImpl; + public IOpenAIRealtimeServiceServerEvents.IConversation Conversation => ConversationImpl; + public IOpenAIRealtimeServiceServerEvents.IInputAudioBuffer InputAudioBuffer => InputAudioBufferImpl; + public IOpenAIRealtimeServiceServerEvents.IResponse Response => ResponseImpl; + public IOpenAIRealtimeServiceServerEvents.IRateLimits RateLimits => RateLimitsImpl; + + internal void RaiseOnError(object sender, ErrorEvent e) + { + OnError?.Invoke(sender, e); + } + + internal void RaiseOnAll(object sender, string message) + { + OnAll?.Invoke(sender, message); + } + + internal class SessionImplementation : IOpenAIRealtimeServiceServerEvents.ISession + { + public event EventHandler? OnCreated; + public event EventHandler? OnUpdated; + + internal void RaiseOnCreated(object sender, SessionEvent e) + { + OnCreated?.Invoke(sender, e); + } + + internal void RaiseOnUpdated(object sender, SessionEvent e) + { + OnUpdated?.Invoke(sender, e); + } + } + + internal class ConversationImplementation : IOpenAIRealtimeServiceServerEvents.IConversation + { + internal readonly ItemImplementation ItemImpl = new(); + public event EventHandler? OnCreated; + public IOpenAIRealtimeServiceServerEvents.IConversation.IItem Item => ItemImpl; + + internal void RaiseOnCreated(object sender, ConversationCreatedEvent e) + { + OnCreated?.Invoke(sender, e); + } + + internal class ItemImplementation : IOpenAIRealtimeServiceServerEvents.IConversation.IItem + { + internal readonly InputAudioTranscriptionImplementation InputAudioTranscriptionImpl = new(); + public event EventHandler? OnCreated; + public event EventHandler? OnTruncated; + public event EventHandler? OnDeleted; + public IOpenAIRealtimeServiceServerEvents.IConversation.IItem.IInputAudioTranscription InputAudioTranscription => InputAudioTranscriptionImpl; + + internal void RaiseOnCreated(object sender, ConversationItemCreatedEvent e) + { + OnCreated?.Invoke(sender, e); + } + + internal void RaiseOnTruncated(object sender, ConversationItemTruncatedEvent e) + { + OnTruncated?.Invoke(sender, e); + } + + internal void RaiseOnDeleted(object sender, ConversationItemDeletedEvent e) + { + OnDeleted?.Invoke(sender, e); + } + + internal class InputAudioTranscriptionImplementation : IOpenAIRealtimeServiceServerEvents.IConversation.IItem.IInputAudioTranscription + { + public event EventHandler? OnCompleted; + public event EventHandler? OnFailed; + + internal void RaiseOnCompleted(object sender, InputAudioTranscriptionCompletedEvent e) + { + OnCompleted?.Invoke(sender, e); + } + + internal void RaiseOnFailed(object sender, InputAudioTranscriptionFailedEvent e) + { + OnFailed?.Invoke(sender, e); + } + } + } + } + + internal class InputAudioBufferImplementation : IOpenAIRealtimeServiceServerEvents.IInputAudioBuffer + { + public event EventHandler? OnCommitted; + public event EventHandler? OnCleared; + public event EventHandler? OnSpeechStarted; + public event EventHandler? OnSpeechStopped; + + internal void RaiseOnCommitted(object sender, AudioBufferCommittedEvent e) + { + OnCommitted?.Invoke(sender, e); + } + + internal void RaiseOnCleared(object sender, AudioBufferClearedEvent e) + { + OnCleared?.Invoke(sender, e); + } + + internal void RaiseOnSpeechStarted(object sender, AudioBufferSpeechStartedEvent e) + { + OnSpeechStarted?.Invoke(sender, e); + } + + internal void RaiseOnSpeechStopped(object sender, AudioBufferSpeechStoppedEvent e) + { + OnSpeechStopped?.Invoke(sender, e); + } + } + + internal class ResponseImplementation : IOpenAIRealtimeServiceServerEvents.IResponse + { + internal readonly AudioImplementation AudioImpl = new(); + internal readonly AudioTranscriptImplementation AudioTranscriptImpl = new(); + internal readonly ContentPartImplementation ContentPartImpl = new(); + internal readonly FunctionCallArgumentsImplementation FunctionCallArgumentsImpl = new(); + + internal readonly OutputItemImplementation OutputItemImpl = new(); + internal readonly TextImplementation TextImpl = new(); + public event EventHandler? OnCreated; + public event EventHandler? OnDone; + + public IOpenAIRealtimeServiceServerEvents.IResponse.IOutputItem OutputItem => OutputItemImpl; + public IOpenAIRealtimeServiceServerEvents.IResponse.IContentPart ContentPart => ContentPartImpl; + public IOpenAIRealtimeServiceServerEvents.IResponse.IText Text => TextImpl; + public IOpenAIRealtimeServiceServerEvents.IResponse.IAudioTranscript AudioTranscript => AudioTranscriptImpl; + public IOpenAIRealtimeServiceServerEvents.IResponse.IAudio Audio => AudioImpl; + public IOpenAIRealtimeServiceServerEvents.IResponse.IFunctionCallArguments FunctionCallArguments => FunctionCallArgumentsImpl; + + internal void RaiseOnCreated(object sender, ResponseEvent e) + { + OnCreated?.Invoke(sender, e); + } + + internal void RaiseOnDone(object sender, ResponseEvent e) + { + OnDone?.Invoke(sender, e); + } + + internal class OutputItemImplementation : IOpenAIRealtimeServiceServerEvents.IResponse.IOutputItem + { + public event EventHandler? OnAdded; + public event EventHandler? OnDone; + + internal void RaiseOnAdded(object sender, ResponseOutputItemAddedEvent e) + { + OnAdded?.Invoke(sender, e); + } + + internal void RaiseOnDone(object sender, ResponseOutputItemDoneEvent e) + { + OnDone?.Invoke(sender, e); + } + } + + internal class ContentPartImplementation : IOpenAIRealtimeServiceServerEvents.IResponse.IContentPart + { + public event EventHandler? OnAdded; + public event EventHandler? OnDone; + + internal void RaiseOnAdded(object sender, ResponseContentPartEvent e) + { + OnAdded?.Invoke(sender, e); + } + + internal void RaiseOnDone(object sender, ResponseContentPartEvent e) + { + OnDone?.Invoke(sender, e); + } + } + + internal class TextImplementation : IOpenAIRealtimeServiceServerEvents.IResponse.IText + { + public event EventHandler? OnDelta; + public event EventHandler? OnDone; + + internal void RaiseOnDelta(object sender, TextStreamEvent e) + { + OnDelta?.Invoke(sender, e); + } + + internal void RaiseOnDone(object sender, TextStreamEvent e) + { + OnDone?.Invoke(sender, e); + } + } + + internal class AudioTranscriptImplementation : IOpenAIRealtimeServiceServerEvents.IResponse.IAudioTranscript + { + public event EventHandler? OnDelta; + public event EventHandler? OnDone; + + internal void RaiseOnDelta(object sender, AudioTranscriptStreamEvent e) + { + OnDelta?.Invoke(sender, e); + } + + internal void RaiseOnDone(object sender, AudioTranscriptStreamEvent e) + { + OnDone?.Invoke(sender, e); + } + } + + internal class AudioImplementation : IOpenAIRealtimeServiceServerEvents.IResponse.IAudio + { + public event EventHandler? OnDelta; + public event EventHandler? OnDone; + + internal void RaiseOnDelta(object sender, AudioStreamEvent e) + { + OnDelta?.Invoke(sender, e); + } + + internal void RaiseOnDone(object sender, AudioStreamEvent e) + { + OnDone?.Invoke(sender, e); + } + } + + internal class FunctionCallArgumentsImplementation : IOpenAIRealtimeServiceServerEvents.IResponse.IFunctionCallArguments + { + public event EventHandler? OnDelta; + public event EventHandler? OnDone; + + internal void RaiseOnDelta(object sender, FunctionCallStreamEvent e) + { + OnDelta?.Invoke(sender, e); + } + + internal void RaiseOnDone(object sender, FunctionCallStreamEvent e) + { + OnDone?.Invoke(sender, e); + } + } + } + + internal class RateLimitsImplementation : IOpenAIRealtimeServiceServerEvents.IRateLimits + { + public event EventHandler? OnUpdated; + + internal void RaiseOnUpdated(object sender, RateLimitsEvent e) + { + OnUpdated?.Invoke(sender, e); + } + } +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/Models.cs b/OpenAI.SDK/ObjectModels/Models.cs index f1ab3970..4c2e6474 100644 --- a/OpenAI.SDK/ObjectModels/Models.cs +++ b/OpenAI.SDK/ObjectModels/Models.cs @@ -117,7 +117,9 @@ public enum Model O1_preview, O1_preview_2024_09_12, O1_mini, - O1_mini_2024_09_12 + O1_mini_2024_09_12, + + Gpt_4o_realtime_preview_2024_10_01 } public enum Subject @@ -257,6 +259,9 @@ public enum Subject /// Dynamic model continuously updated to the current version of GPT-4o in ChatGPT. Intended for research and evaluation [1]. /// public static string Chatgpt_4o_latest => "chatgpt-4o-latest"; + + + public static string Gpt_4o_realtime_preview_2024_10_01 => "gpt-4o-realtime-preview-2024-10-01"; public static string Ada => "ada"; @@ -524,6 +529,7 @@ public static string EnumToString(this Model model) Model.O1_mini_2024_09_12 => O1_mini_2024_09_12, Model.O1_preview => O1_preview, Model.O1_preview_2024_09_12 => O1_preview_2024_09_12, + Model.Gpt_4o_realtime_preview_2024_10_01 => Gpt_4o_realtime_preview_2024_10_01, _ => throw new ArgumentOutOfRangeException(nameof(model), model, null) }; } diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs new file mode 100644 index 00000000..317eed20 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs @@ -0,0 +1,50 @@ +namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + +///

+/// Contains constants used by the Realtime API implementation. +/// +public static class RealtimeConstants +{ + /// + /// WebSocket subprotocols used for connection. + /// + public static class SubProtocols + { + public const string Realtime = "realtime"; + public const string Beta = "openai-beta.realtime-v1"; + } + + /// + /// Headers used in API requests. + /// + public static class Headers + { + public const string Authorization = "Authorization"; + public const string OpenAIBeta = "OpenAI-Beta"; + public const string OpenAIOrganization = "OpenAI-Organization"; + } + + /// + /// Audio format settings. + /// + public static class Audio + { + public const string FormatPcm16 = "pcm16"; + public const string FormatG711Ulaw = "g711_ulaw"; + public const string FormatG711Alaw = "g711_alaw"; + public const int DefaultSampleRate = 24000; + public const int DefaultBitsPerSample = 16; + public const int DefaultChannels = 1; + } + + /// + /// Turn detection settings. + /// + public static class TurnDetection + { + public const string TypeServerVad = "server_vad"; + public const double DefaultThreshold = 0.5; + public const int DefaultPrefixPaddingMs = 300; + public const int DefaultSilenceDurationMs = 200; + } +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs new file mode 100644 index 00000000..bb92ee94 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs @@ -0,0 +1,197 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + +internal class StatusJsonConverter : JsonConverter +{ + public override Status Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value switch + { + "completed" => Status.Completed, + "in_progress" => Status.InProgress, + "incomplete" => Status.Incomplete, + "cancelled" => Status.Cancelled, + "failed" => Status.Failed, + _ => Status.Unimplemented // Return Unimplemented for unknown values + }; + } + + public override void Write(Utf8JsonWriter writer, Status value, JsonSerializerOptions options) + { + var stringValue = value switch + { + Status.Completed => "completed", + Status.InProgress => "in_progress", + Status.Incomplete => "incomplete", + Status.Cancelled => "cancelled", + Status.Failed => "failed", + _ => "unimplemented" + }; + writer.WriteStringValue(stringValue); + } +} + +internal class AudioFormatJsonConverter : JsonConverter +{ + public override AudioFormat Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value switch + { + RealtimeConstants.Audio.FormatPcm16 => AudioFormat.PCM16, + RealtimeConstants.Audio.FormatG711Ulaw => AudioFormat.G711_ULAW, + RealtimeConstants.Audio.FormatG711Alaw => AudioFormat.G711_ALAW, + _ => AudioFormat.Unimplemented + }; + } + + public override void Write(Utf8JsonWriter writer, AudioFormat value, JsonSerializerOptions options) + { + var stringValue = value switch + { + AudioFormat.PCM16 => RealtimeConstants.Audio.FormatPcm16, + AudioFormat.G711_ULAW => RealtimeConstants.Audio.FormatG711Ulaw, + AudioFormat.G711_ALAW => RealtimeConstants.Audio.FormatG711Alaw, + _ => "unimplemented" + }; + writer.WriteStringValue(stringValue); + } +} + +internal class ContentTypeJsonConverter : JsonConverter +{ + public override ContentType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value switch + { + "input_text" => ContentType.InputText, + "input_audio" => ContentType.InputAudio, + "text" => ContentType.Text, + "audio" => ContentType.Audio, + _ => ContentType.Unimplemented + }; + } + + public override void Write(Utf8JsonWriter writer, ContentType value, JsonSerializerOptions options) + { + var stringValue = value switch + { + ContentType.InputText => "input_text", + ContentType.InputAudio => "input_audio", + ContentType.Text => "text", + ContentType.Audio => "audio", + _ => "unimplemented" + }; + writer.WriteStringValue(stringValue); + } +} + +internal class ItemTypeJsonConverter : JsonConverter +{ + public override ItemType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value switch + { + "message" => ItemType.Message, + "function_call" => ItemType.FunctionCall, + "function_call_output" => ItemType.FunctionCallOutput, + _ => ItemType.Unimplemented + }; + } + + public override void Write(Utf8JsonWriter writer, ItemType value, JsonSerializerOptions options) + { + var stringValue = value switch + { + ItemType.Message => "message", + ItemType.FunctionCall => "function_call", + ItemType.FunctionCallOutput => "function_call_output", + _ => "unimplemented" + }; + writer.WriteStringValue(stringValue); + } +} + +internal class RoleJsonConverter : JsonConverter +{ + public override Role Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value switch + { + "user" => Role.User, + "assistant" => Role.Assistant, + "system" => Role.System, + _ => Role.Unimplemented + }; + } + + public override void Write(Utf8JsonWriter writer, Role value, JsonSerializerOptions options) + { + var stringValue = value switch + { + Role.User => "user", + Role.Assistant => "assistant", + Role.System => "system", + _ => "unimplemented" + }; + writer.WriteStringValue(stringValue); + } +} + +[JsonConverter(typeof(StatusJsonConverter))] +public enum Status +{ + Unimplemented, + Completed, + InProgress, + Incomplete, + Cancelled, + Failed +} + +[JsonConverter(typeof(AudioFormatJsonConverter))] +public enum AudioFormat +{ + Unimplemented, + PCM16, + + // ReSharper disable once InconsistentNaming + G711_ULAW, + + // ReSharper disable once InconsistentNaming + G711_ALAW +} + +[JsonConverter(typeof(ContentTypeJsonConverter))] +public enum ContentType +{ + Unimplemented, + InputText, + InputAudio, + Text, + Audio +} + +[JsonConverter(typeof(ItemTypeJsonConverter))] +public enum ItemType +{ + Unimplemented, + Message, + FunctionCall, + FunctionCallOutput +} + +[JsonConverter(typeof(RoleJsonConverter))] +public enum Role +{ + Unimplemented, + User, + Assistant, + System +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs new file mode 100644 index 00000000..30b99179 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs @@ -0,0 +1,119 @@ +namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + +public static class RealtimeEventTypes +{ + public static class Client + { + public static class Session + { + public const string Update = "session.update"; + } + + public static class InputAudioBuffer + { + public const string Append = "input_audio_buffer.append"; + public const string Commit = "input_audio_buffer.commit"; + public const string Clear = "input_audio_buffer.clear"; + } + + public static class Conversation + { + public static class Item + { + public const string Create = "conversation.item.create"; + public const string Truncate = "conversation.item.truncate"; + public const string Delete = "conversation.item.delete"; + } + } + + public static class Response + { + public const string Create = "response.create"; + public const string Cancel = "response.cancel"; + } + } + + public static class Server + { + public const string Error = "error"; + + public static class Session + { + public const string Created = "session.created"; + public const string Updated = "session.updated"; + } + + public static class Conversation + { + public const string Created = "conversation.created"; + + public static class Item + { + public const string Created = "conversation.item.created"; + public const string Truncated = "conversation.item.truncated"; + public const string Deleted = "conversation.item.deleted"; + + public static class InputAudioTranscription + { + public const string Completed = "conversation.item.input_audio_transcription.completed"; + public const string Failed = "conversation.item.input_audio_transcription.failed"; + } + } + } + + public static class InputAudioBuffer + { + public const string Committed = "input_audio_buffer.committed"; + public const string Cleared = "input_audio_buffer.cleared"; + public const string SpeechStarted = "input_audio_buffer.speech_started"; + public const string SpeechStopped = "input_audio_buffer.speech_stopped"; + } + + public static class Response + { + public const string Created = "response.created"; + public const string Done = "response.done"; + + public static class OutputItem + { + public const string Added = "response.output_item.added"; + public const string Done = "response.output_item.done"; + } + + public static class ContentPart + { + public const string Added = "response.content_part.added"; + public const string Done = "response.content_part.done"; + } + + public static class Text + { + public const string Delta = "response.text.delta"; + public const string Done = "response.text.done"; + } + + public static class AudioTranscript + { + public const string Delta = "response.audio_transcript.delta"; + public const string Done = "response.audio_transcript.done"; + } + + public static class Audio + { + public const string Delta = "response.audio.delta"; + public const string Done = "response.audio.done"; + } + + public static class FunctionCallArguments + { + public const string Delta = "response.function_call_arguments.delta"; + public const string Done = "response.function_call_arguments.done"; + } + } + + public static class RateLimits + { + public const string Updated = "rate_limits.updated"; + } + } +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeObjectModels.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeObjectModels.cs new file mode 100644 index 00000000..466b9b31 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeObjectModels.cs @@ -0,0 +1,1205 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; +using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; +using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; + +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable UnusedType.Global + +namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + +#region Base Classes + +/// +/// Base class for all events in the Realtime API. +/// +public abstract class EventBase +{ + protected EventBase() + { + } + + protected EventBase(string clientEventType) + { + Type = clientEventType; + } + + /// + /// Optional client-generated ID used to identify this event + /// + [JsonPropertyName("event_id")] + public string? EventId { get; set; } + + /// + /// The event type that identifies the kind of event + /// + [JsonPropertyName("type")] + public string Type { get; set; } +} + +/// +/// Base class for error events returned when an error occurs. +/// +public abstract class ErrorEventBase : EventBase +{ + /// + /// Details of the error. + /// + [JsonPropertyName("error")] + public Error Error { get; set; } +} + +#endregion + +#region Session Models + +/// +/// Configuration for a session. +/// +public class SessionConfig +{ + /// + /// The set of modalities the model can respond with. To disable audio, set this to ["text"]. + /// + [JsonPropertyName("modalities")] + public List? Modalities { get; set; } + + /// + /// The default system instructions prepended to model calls. + /// + [JsonPropertyName("instructions")] + public string? Instructions { get; set; } + + /// + /// The voice the model uses to respond. + /// Cannot be changed once the model has responded with audio at least once. + /// + [JsonPropertyName("voice")] + public string? Voice { get; set; } + + /// + /// The format of input audio. + /// + [JsonPropertyName("input_audio_format")] + public AudioFormat? InputAudioFormat { get; set; } + + /// + /// The format of output audio. + /// + [JsonPropertyName("output_audio_format")] + public AudioFormat? OutputAudioFormat { get; set; } + + /// + /// Configuration for input audio transcription. Can be set to null to turn off. + /// + [JsonPropertyName("input_audio_transcription")] + public AudioTranscriptionConfig? InputAudioTranscription { get; set; } + + /// + /// Configuration for turn detection. Can be set to null to turn off. + /// + [JsonPropertyName("turn_detection")] + public TurnDetectionConfig? TurnDetection { get; set; } + + /// + /// Tools (functions) available to the model. + /// + [JsonPropertyName("tools")] + public List? Tools { get; set; } + + /// + /// How the model chooses tools. + /// + /// + [JsonPropertyName("tool_choice")] + public string? ToolChoice { get; set; } + + /// + /// Sampling temperature for the model. + /// + [JsonPropertyName("temperature")] + public double? Temperature { get; set; } + + + /// + /// Maximum number of output tokens for a single assistant response. + /// Can be an integer between 1 and 4096, or "inf" for maximum available tokens. + /// + [JsonPropertyName("max_output_tokens")] + public MaxOutputTokens? MaxOutputTokens { get; set; } + + // Example usage method + public void SetMaxTokens(int tokens) + { + MaxOutputTokens = MaxOutputTokens.FromInt(tokens); + } + + public void SetInfiniteTokens() + { + MaxOutputTokens = MaxOutputTokens.Infinite(); + } +} + +public class RealtimeToolDefinition +{ + /// + /// The type of the tool, i.e. function. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// The name of the function to be called. Must be a-z, A-Z, 0-9, + /// or contain underscores and dashes, with a maximum length of 64. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// A description of what the function does, used by the model to choose when and how to call the function. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Optional. The parameters the functions accepts, described as a JSON Schema object. + /// See the guide for examples, + /// and the JSON Schema reference for + /// documentation about the format. + /// + [JsonPropertyName("parameters")] + public PropertyDefinition Parameters { get; set; } +} + +/// +/// Configuration for audio transcription. +/// +public class AudioTranscriptionConfig +{ + /// + /// The model to use for transcription (e.g., "whisper-1"). + /// + [JsonPropertyName("model")] + public string Model { get; set; } +} + +/// +/// Configuration for turn detection. +/// +public class TurnDetectionConfig +{ + /// + /// Type of turn detection, only "server_vad" is currently supported. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Activation threshold for VAD (0.0 to 1.0). + /// + [JsonPropertyName("threshold")] + public double Threshold { get; set; } + + /// + /// Amount of audio to include before speech starts (in milliseconds). + /// + [JsonPropertyName("prefix_padding_ms")] + public int PrefixPaddingMs { get; set; } + + /// + /// Duration of silence to detect speech stop (in milliseconds). + /// + [JsonPropertyName("silence_duration_ms")] + public int SilenceDurationMs { get; set; } +} + +/// +/// JSON Schema for function parameters. +/// +public class JsonSchema +{ + /// + /// The type of the schema (e.g., "object"). + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Properties of the schema. + /// + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } + + /// + /// Required properties. + /// + [JsonPropertyName("required")] + public List? Required { get; set; } +} + +/// +/// JSON Schema property definition. +/// +public class JsonSchemaProperty +{ + /// + /// The type of the property. + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Description of the property. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +#endregion + +#region Conversation Models + +/// +/// Represents an item in the conversation. +/// +public class ConversationItem +{ + /// + /// The unique ID of the item. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// The type of the item. + /// + [JsonPropertyName("type")] + public ItemType Type { get; set; } + + ///// + ///// The status of the item. + ///// + [JsonPropertyName("status")] + public Status? Status { get; set; } + + /// + /// The role of the message sender. + /// + [JsonPropertyName("role")] + public Role? Role { get; set; } + + /// + /// The content of the message. + /// + [JsonPropertyName("content")] + public List? Content { get; set; } + + /// + /// The ID of the function call (for "function_call" items). + /// + [JsonPropertyName("call_id")] + public string? CallId { get; set; } + + /// + /// The name of the function being called (for "function_call" items). + /// + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// The arguments of the function call (for "function_call" items). + /// + [JsonPropertyName("arguments")] + public string? Arguments { get; set; } + + /// + /// The output of the function call (for "function_call_output" items). + /// + [JsonPropertyName("output")] + public string? Output { get; set; } +} + +/// +/// Represents a content part within a message. +/// +public class ContentPart +{ + /// + /// The content type. + /// + [JsonPropertyName("type")] + public ContentType Type { get; set; } + + /// + /// The text content. + /// + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// + /// Base64-encoded audio data. + /// + [JsonPropertyName("audio")] + public string? Audio { get; set; } + + /// + /// The transcript of the audio. + /// + [JsonPropertyName("transcript")] + public string? Transcript { get; set; } +} + +#endregion + +#region Request Events + +/// +/// Request to update the session's default configuration +/// +public class SessionUpdateRequest() : EventBase(RealtimeEventTypes.Client.Session.Update) +{ + /// + /// Session configuration to update + /// + [JsonPropertyName("session")] + public SessionConfig Session { get; set; } +} + +/// +/// Request to append audio bytes to the input audio buffer +/// +public class AudioBufferAppendRequest() : EventBase(RealtimeEventTypes.Client.InputAudioBuffer.Append) +{ + /// + /// Base64-encoded audio bytes + /// + [JsonPropertyName("audio")] + public string Audio { get; set; } +} + +/// +/// Request to commit audio bytes to a user message +/// +public class AudioBufferCommitRequest() : EventBase(RealtimeEventTypes.Client.InputAudioBuffer.Commit); + +/// +/// Request to clear the audio bytes in the buffer +/// +public class AudioBufferClearRequest() : EventBase(RealtimeEventTypes.Client.InputAudioBuffer.Clear); + +/// +/// Request to create a conversation item +/// +public class ConversationItemCreateRequest() : EventBase(RealtimeEventTypes.Client.Conversation.Item.Create) +{ + /// + /// The ID of the preceding item after which the new item will be inserted + /// + [JsonPropertyName("previous_item_id")] + public string? PreviousItemId { get; set; } + + /// + /// The item to add to the conversation + /// + [JsonPropertyName("item")] + public ConversationItem Item { get; set; } +} + +/// +/// Request to truncate a previous assistant message's audio +/// +public class ConversationItemTruncateRequest() : EventBase(RealtimeEventTypes.Client.Conversation.Item.Truncate) +{ + /// + /// The ID of the assistant message item to truncate + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + + /// + /// The index of the content part to truncate + /// + [JsonPropertyName("content_index")] + public int ContentIndex { get; set; } + + /// + /// Inclusive duration up to which audio is truncated, in milliseconds + /// + [JsonPropertyName("audio_end_ms")] + public int AudioEndMs { get; set; } +} + +/// +/// Request to delete a conversation item +/// +public class ConversationItemDeleteRequest() : EventBase(RealtimeEventTypes.Client.Conversation.Item.Delete) +{ + /// + /// The ID of the item to delete + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } +} + +/// +/// Request to trigger a response generation +/// +public class ResponseCreateRequest() : EventBase(RealtimeEventTypes.Client.Response.Create) +{ + /// + /// Configuration for the response + /// + [JsonPropertyName("response")] + public ResponseConfig Response { get; set; } +} + +/// +/// Configuration for generating a response. +/// +public class ResponseConfig +{ + /// + /// The modalities for the response. + /// + [JsonPropertyName("modalities")] + public List? Modalities { get; set; } + + /// + /// Instructions for the model. + /// + [JsonPropertyName("instructions")] + public string? Instructions { get; set; } + + /// + /// The voice the model uses to respond. + /// + [JsonPropertyName("voice")] + public string? Voice { get; set; } + + /// + /// The format of output audio. + /// + [JsonPropertyName("output_audio_format")] + public AudioFormat? OutputAudioFormat { get; set; } + + /// + /// Tools (functions) available to the model. + /// + [JsonPropertyName("tools")] + public List? Tools { get; set; } + + /// + /// How the model chooses tools. + /// + /// + [JsonPropertyName("tool_choice")] + public string? ToolChoice { get; set; } + + /// + /// Sampling temperature. + /// + [JsonPropertyName("temperature")] + public double? Temperature { get; set; } + + /// + /// Maximum number of output tokens for a single assistant response. + /// Can be an integer between 1 and 4096, or "inf" for maximum available tokens. + /// + [JsonPropertyName("max_output_tokens")] + public MaxOutputTokens? MaxOutputTokens { get; set; } +} + +/// +/// Request to cancel an in-progress response +/// +public class ResponseCancelRequest() : EventBase(RealtimeEventTypes.Client.Response.Cancel); + +#endregion + +#region Server Events + +/// +/// Error event returned when an error occurs. +/// +public class ErrorEvent : ErrorEventBase; + +/// +/// Event returned when a session is created or updated. +/// +public class SessionEvent : EventBase +{ + /// + /// The session resource. + /// + [JsonPropertyName("session")] + public SessionResource Session { get; set; } +} + +/// +/// The session resource. +/// +public class SessionResource +{ + /// + /// The unique ID of the session. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// The object type, must be "realtime.session". + /// + [JsonPropertyName("object")] + public string Object { get; set; } = "realtime.session"; + + /// + /// The default model used for this session. + /// + [JsonPropertyName("model")] + public string Model { get; set; } + + /// + /// The set of modalities the model can respond with. + /// + [JsonPropertyName("modalities")] + public List Modalities { get; set; } + + /// + /// The default system instructions. + /// + [JsonPropertyName("instructions")] + public string? Instructions { get; set; } + + /// + /// The voice the model uses to respond. + /// + [JsonPropertyName("voice")] + public string Voice { get; set; } + + /// + /// The format of input audio. + /// + [JsonPropertyName("input_audio_format")] + public AudioFormat InputAudioFormat { get; set; } + + /// + /// The format of output audio. + /// + [JsonPropertyName("output_audio_format")] + public AudioFormat OutputAudioFormat { get; set; } + + /// + /// Configuration for input audio transcription. + /// + [JsonPropertyName("input_audio_transcription")] + public AudioTranscriptionConfig? InputAudioTranscription { get; set; } + + /// + /// Configuration for turn detection. + /// + [JsonPropertyName("turn_detection")] + public TurnDetectionConfig? TurnDetection { get; set; } + + /// + /// Tools (functions) available to the model. + /// + [JsonPropertyName("tools")] + public List Tools { get; set; } + + /// + /// How the model chooses tools. + /// + /// + [JsonPropertyName("tool_choice")] + public string ToolChoice { get; set; } + + /// + /// Sampling temperature. + /// + [JsonPropertyName("temperature")] + public double Temperature { get; set; } + + /// + /// Maximum number of output tokens. + /// + [JsonPropertyName("max_output_tokens")] + public MaxOutputTokens? MaxOutputTokens { get; set; } +} + +/// +/// Event returned when a conversation is created. +/// +public class ConversationCreatedEvent : EventBase +{ + /// + /// The conversation resource. + /// + [JsonPropertyName("conversation")] + public ConversationResource Conversation { get; set; } +} + +/// +/// The conversation resource. +/// +public class ConversationResource +{ + /// + /// The unique ID of the conversation. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// The object type, must be "realtime.conversation". + /// + [JsonPropertyName("object")] + public string Object { get; set; } = "realtime.conversation"; +} + +/// +/// Event returned when a conversation item is created. +/// +public class ConversationItemCreatedEvent : EventBase +{ + /// + /// The ID of the preceding item. + /// + [JsonPropertyName("previous_item_id")] + public string? PreviousItemId { get; set; } + + /// + /// The item that was created. + /// + [JsonPropertyName("item")] + public ConversationItem Item { get; set; } +} + +/// +/// Event returned when input audio transcription is enabled and transcription succeeds. +/// +public class InputAudioTranscriptionCompletedEvent : EventBase +{ + /// + /// The ID of the user message item. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + + /// + /// The index of the content part containing the audio. + /// + [JsonPropertyName("content_index")] + public int ContentIndex { get; set; } + + /// + /// The transcribed text. + /// + [JsonPropertyName("transcript")] + public string Transcript { get; set; } +} + +/// +/// Event returned when input audio transcription fails. +/// +public class InputAudioTranscriptionFailedEvent : EventBase +{ + /// + /// The ID of the user message item. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + + /// + /// The index of the content part containing the audio. + /// + [JsonPropertyName("content_index")] + public int ContentIndex { get; set; } + + /// + /// Details of the transcription error. + /// + [JsonPropertyName("error")] + public Error Error { get; set; } +} + +/// +/// Event returned when an earlier assistant audio message item is truncated. +/// +public class ConversationItemTruncatedEvent : EventBase +{ + /// + /// The ID of the assistant message item that was truncated. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + + /// + /// The index of the content part that was truncated. + /// + [JsonPropertyName("content_index")] + public int ContentIndex { get; set; } + + /// + /// The duration up to which the audio was truncated, in milliseconds. + /// + [JsonPropertyName("audio_end_ms")] + public int AudioEndMs { get; set; } +} + +/// +/// Event returned when an item in the conversation is deleted. +/// +public class ConversationItemDeletedEvent : EventBase +{ + /// + /// The ID of the item that was deleted. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } +} + +/// +/// Event returned when an input audio buffer is committed. +/// +public class AudioBufferCommittedEvent : EventBase +{ + /// + /// The ID of the preceding item after which the new item will be inserted. + /// + [JsonPropertyName("previous_item_id")] + public string PreviousItemId { get; set; } + + /// + /// The ID of the user message item that will be created. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } +} + +/// +/// Event returned when the input audio buffer is cleared. +/// +public class AudioBufferClearedEvent : EventBase; + +/// +/// Event returned in server turn detection mode when speech is detected. +/// +public class AudioBufferSpeechStartedEvent : EventBase +{ + /// + /// Milliseconds since the session started when speech was detected. + /// + [JsonPropertyName("audio_start_ms")] + public int AudioStartMs { get; set; } + + /// + /// The ID of the user message item that will be created when speech stops. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } +} + +/// +/// Event returned in server turn detection mode when speech stops. +/// +public class AudioBufferSpeechStoppedEvent : EventBase +{ + /// + /// Milliseconds since the session started when speech stopped. + /// + [JsonPropertyName("audio_end_ms")] + public int AudioEndMs { get; set; } + + /// + /// The ID of the user message item that will be created. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } +} + +/// +/// Event returned when a Response is created or done. +/// +public class ResponseEvent : EventBase +{ + /// + /// The response resource. + /// + [JsonPropertyName("response")] + public ResponseResource Response { get; set; } +} + +/// +/// The response resource. +/// +public class ResponseResource +{ + /// + /// The unique ID of the response. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// The object type, must be "realtime.response". + /// + [JsonPropertyName("object")] + public string Object { get; set; } = "realtime.response"; + + /// + /// The status of the response. + /// + [JsonPropertyName("status")] + public Status Status { get; set; } + + /// + /// Additional details about the status. + /// + [JsonPropertyName("status_details")] + public Dictionary? StatusDetails { get; set; } + + /// + /// The list of output items generated by the response. + /// + [JsonPropertyName("output")] + public List Output { get; set; } + + /// + /// Usage statistics for the response. + /// + [JsonPropertyName("usage")] + public UsageResponse? Usage { get; set; } +} + +/// +/// Event returned when a new Item is created during response generation. +/// +public class ResponseOutputItemAddedEvent : EventBase +{ + /// + /// The ID of the response to which the item belongs. + /// + [JsonPropertyName("response_id")] + public string ResponseId { get; set; } + + /// + /// The index of the output item in the response. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; set; } + + /// + /// The item that was added. + /// + [JsonPropertyName("item")] + public ConversationItem Item { get; set; } +} + +/// +/// Event returned when an Item is done streaming. +/// +public class ResponseOutputItemDoneEvent : EventBase +{ + /// + /// The ID of the response to which the item belongs. + /// + [JsonPropertyName("response_id")] + public string ResponseId { get; set; } + + /// + /// The index of the output item in the response. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; set; } + + /// + /// The completed item. + /// + [JsonPropertyName("item")] + public ConversationItem Item { get; set; } +} + +/// +/// Event returned when new content part is added. +/// +public class ResponseContentPartEvent : EventBase +{ + /// + /// The ID of the response. + /// + [JsonPropertyName("response_id")] + public string ResponseId { get; set; } + + /// + /// The ID of the item. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + + /// + /// The index of the output item in the response. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; set; } + + /// + /// The index of the content part in the item's content array. + /// + [JsonPropertyName("content_index")] + public int ContentIndex { get; set; } + + /// + /// The content part. + /// + [JsonPropertyName("part")] + public ContentPart Part { get; set; } +} + +/// +/// Base class for streaming events with content information. +/// +public abstract class ContentStreamEventBase : EventBase +{ + /// + /// The ID of the response. + /// + [JsonPropertyName("response_id")] + public string ResponseId { get; set; } + + /// + /// The ID of the item. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + + /// + /// The index of the output item in the response. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; set; } + + /// + /// The index of the content part in the item's content array. + /// + [JsonPropertyName("content_index")] + public int ContentIndex { get; set; } +} + +/// +/// Event for streaming text content. +/// +public class TextStreamEvent : ContentStreamEventBase +{ + /// + /// The text delta. + /// + [JsonPropertyName("delta")] + public string? Delta { get; set; } + + /// + /// The complete text content (for done events). + /// + [JsonPropertyName("text")] + public string? Text { get; set; } +} + +/// +/// Event for streaming audio transcript content. +/// +public class AudioTranscriptStreamEvent : ContentStreamEventBase +{ + /// + /// The transcript delta. + /// + [JsonPropertyName("delta")] + public string? Delta { get; set; } + + /// + /// The complete transcript (for done events). + /// + [JsonPropertyName("transcript")] + public string? Transcript { get; set; } +} + +/// +/// Event for streaming audio content. +/// +public class AudioStreamEvent : ContentStreamEventBase +{ + /// + /// Base64-encoded audio data delta. + /// + [JsonPropertyName("delta")] + public string? Delta { get; set; } +} + +/// +/// Event for streaming function call arguments. +/// +public class FunctionCallStreamEvent : EventBase +{ + /// + /// The ID of the response. + /// + [JsonPropertyName("response_id")] + public string ResponseId { get; set; } + + /// + /// The ID of the function call item. + /// + [JsonPropertyName("item_id")] + public string ItemId { get; set; } + + /// + /// The index of the output item in the response. + /// + [JsonPropertyName("output_index")] + public int OutputIndex { get; set; } + + /// + /// The ID of the function call. + /// + [JsonPropertyName("call_id")] + public string CallId { get; set; } + + /// + /// The arguments delta as a JSON string. + /// + [JsonPropertyName("delta")] + public string? Delta { get; set; } + + /// + /// The final arguments as a JSON string. + /// + [JsonPropertyName("arguments")] + public string? Arguments { get; set; } + + /// + /// The final arguments as a JSON string. + /// + [JsonPropertyName("name")] + public string? Name { get; set; } +} + +/// +/// Event containing rate limit information. +/// +public class RateLimitsEvent : EventBase +{ + /// + /// List of rate limit information. + /// + [JsonPropertyName("rate_limits")] + public List RateLimits { get; set; } +} + +/// +/// Information about a specific rate limit. +/// +public class RateLimit +{ + /// + /// The name of the rate limit ("requests", "tokens", "input_tokens", "output_tokens"). + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// The maximum allowed value for the rate limit. + /// + [JsonPropertyName("limit")] + public int Limit { get; set; } + + /// + /// The remaining value before the limit is reached. + /// + [JsonPropertyName("remaining")] + public int Remaining { get; set; } + + /// + /// Seconds until the rate limit resets. + /// + [JsonPropertyName("reset_seconds")] + public double ResetSeconds { get; set; } +} + +#endregion + +/// +/// Represents the maximum output tokens setting which can be either an integer or "inf" +/// +[JsonConverter(typeof(MaxOutputTokensConverter))] +public class MaxOutputTokens +{ + private readonly bool _isInfinite; + private readonly int? _value; + + private MaxOutputTokens(int? value, bool isInfinite) + { + _value = value; + _isInfinite = isInfinite; + } + + /// + /// Gets the integer value if set, or null if infinite + /// + public int? Value => _value; + + /// + /// Gets whether this represents infinite tokens + /// + public bool IsInfinite => _isInfinite; + + /// + /// Creates a MaxOutputTokens instance with a specific token limit + /// + /// Token limit between 1 and 4096 + /// MaxOutputTokens instance + public static MaxOutputTokens FromInt(int value) + { + return new(value, false); + } + + /// + /// Creates a MaxOutputTokens instance representing infinite tokens + /// + /// MaxOutputTokens instance + public static MaxOutputTokens Infinite() + { + return new(null, true); + } + + public override string ToString() + { + return IsInfinite ? "inf" : Value?.ToString() ?? ""; + } +} + +/// +/// JSON converter for MaxOutputTokens to handle both integer and "inf" values +/// +public class MaxOutputTokensConverter : JsonConverter +{ + public override MaxOutputTokens Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + return reader.TokenType switch + { + JsonTokenType.String when reader.GetString() is "inf" => MaxOutputTokens.Infinite(), + JsonTokenType.Number => MaxOutputTokens.FromInt(reader.GetInt32()), + _ => throw new JsonException("Value must be an integer or 'inf'") + }; + } + + public override void Write(Utf8JsonWriter writer, MaxOutputTokens value, JsonSerializerOptions options) + { + if (value.IsInfinite) + writer.WriteStringValue("inf"); + else if (value.Value.HasValue) + writer.WriteNumberValue(value.Value.Value); + else + writer.WriteNullValue(); + } +} \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeServiceJsonContext.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeServiceJsonContext.cs new file mode 100644 index 00000000..4975e480 --- /dev/null +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeServiceJsonContext.cs @@ -0,0 +1,76 @@ +#if NET6_0_OR_GREATER + +using System.Text.Json; +using System.Text.Json.Serialization; +using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; + +namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; + +[JsonSerializable(typeof(ErrorEvent))] +[JsonSerializable(typeof(SessionEvent))] +[JsonSerializable(typeof(ConversationCreatedEvent))] +[JsonSerializable(typeof(ConversationItemCreatedEvent))] +[JsonSerializable(typeof(InputAudioTranscriptionCompletedEvent))] +[JsonSerializable(typeof(InputAudioTranscriptionFailedEvent))] +[JsonSerializable(typeof(ConversationItemTruncatedEvent))] +[JsonSerializable(typeof(ConversationItemDeletedEvent))] +[JsonSerializable(typeof(AudioBufferCommittedEvent))] +[JsonSerializable(typeof(AudioBufferClearedEvent))] +[JsonSerializable(typeof(AudioBufferSpeechStartedEvent))] +[JsonSerializable(typeof(AudioBufferSpeechStoppedEvent))] +[JsonSerializable(typeof(ResponseEvent))] +[JsonSerializable(typeof(ResponseOutputItemAddedEvent))] +[JsonSerializable(typeof(ResponseOutputItemDoneEvent))] +[JsonSerializable(typeof(ResponseContentPartEvent))] +[JsonSerializable(typeof(TextStreamEvent))] +[JsonSerializable(typeof(AudioTranscriptStreamEvent))] +[JsonSerializable(typeof(AudioStreamEvent))] +[JsonSerializable(typeof(FunctionCallStreamEvent))] +[JsonSerializable(typeof(RateLimitsEvent))] +[JsonSerializable(typeof(SessionUpdateRequest))] + +// Additional Request Events +[JsonSerializable(typeof(AudioBufferAppendRequest))] +[JsonSerializable(typeof(AudioBufferCommitRequest))] +[JsonSerializable(typeof(AudioBufferClearRequest))] +[JsonSerializable(typeof(ConversationItemCreateRequest))] +[JsonSerializable(typeof(ConversationItemTruncateRequest))] +[JsonSerializable(typeof(ConversationItemDeleteRequest))] +[JsonSerializable(typeof(ResponseCreateRequest))] +[JsonSerializable(typeof(ResponseCancelRequest))] +[JsonSerializable(typeof(ResponseConfig))] + +// Model Classes +[JsonSerializable(typeof(SessionConfig))] +[JsonSerializable(typeof(AudioTranscriptionConfig))] +[JsonSerializable(typeof(TurnDetectionConfig))] +[JsonSerializable(typeof(JsonSchema))] +[JsonSerializable(typeof(JsonSchemaProperty))] +[JsonSerializable(typeof(ConversationItem))] +[JsonSerializable(typeof(ContentPart))] +[JsonSerializable(typeof(SessionResource))] +[JsonSerializable(typeof(ConversationResource))] +[JsonSerializable(typeof(ResponseResource))] +[JsonSerializable(typeof(RateLimit))] +[JsonSerializable(typeof(MaxOutputTokens))] + +// Enum Types +[JsonSerializable(typeof(AudioFormat))] +[JsonSerializable(typeof(Status))] +[JsonSerializable(typeof(ContentType))] +[JsonSerializable(typeof(ItemType))] +[JsonSerializable(typeof(Role))] + +// Collections and Dictionary Types +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSourceGenerationOptions(WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, AllowTrailingCommas = false, PropertyNameCaseInsensitive = false, ReadCommentHandling = JsonCommentHandling.Skip)] +internal partial class RealtimeServiceJsonContext : JsonSerializerContext +{ +} +#endif \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs index 5765d4c5..215ecce6 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/BaseResponse.cs @@ -129,6 +129,11 @@ public object MessageObject } } } + /// + /// The event_id of the client event that caused the error, if applicable. + /// + [JsonPropertyName("event_id")] + public string? EventId { get; set; } public class MessageConverter : JsonConverter { diff --git a/OpenAI.SDK/OpenAiOptions.cs b/OpenAI.SDK/OpenAiOptions.cs index b8aa4f03..9f14db5a 100644 --- a/OpenAI.SDK/OpenAiOptions.cs +++ b/OpenAI.SDK/OpenAiOptions.cs @@ -79,6 +79,11 @@ public string ApiVersion set => _apiVersion = value; } + /// + /// Base Real Time Socket Url + /// + public string BaseRealTimeSocketUrl { get; set; } = "wss://api.openai.com/v1/realtime"; + /// /// Base Domain /// diff --git a/OpenAI.sln.DotSettings b/OpenAI.sln.DotSettings index 9290d49a..8659a6dd 100644 --- a/OpenAI.sln.DotSettings +++ b/OpenAI.sln.DotSettings @@ -1,6 +1,8 @@  AI CREOL + PCM + True True True True @@ -18,4 +20,6 @@ True True True - True \ No newline at end of file + True + True + True \ No newline at end of file From 5c4d10ae534abd9ca4427409182d4064b9c97e12 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Sun, 10 Nov 2024 21:38:11 +0000 Subject: [PATCH 06/12] Documentation update --- .../RealtimeHelpers/RealtimeAudioExample.cs | 229 ++++++++-- .../TestHelpers/RealtimeHelpers/VoiceInput.cs | 69 ++- .../RealtimeHelpers/VoiceOutput.cs | 53 ++- .../Builders/OpenAIRealtimeServiceBuilder.cs | 73 +++- ...enAIRealtimeServiceCollectionExtensions.cs | 45 +- OpenAI.SDK/Managers/OpenAIRealtimeService.cs | 108 ++++- .../OpenAIRealtimeServiceClientEvents.cs | 101 +++++ .../OpenAIRealtimeServiceServerEvents.cs | 400 +++++++++++++----- .../RealtimeModels/RealtimeConstants.cs | 75 +++- .../RealtimeModels/RealtimeEnums.cs | 117 ++++- .../RealtimeModels/RealtimeEventTypes.cs | 193 +++++++++ 11 files changed, 1292 insertions(+), 171 deletions(-) diff --git a/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs b/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs index 892e27ab..b6a21e5e 100644 --- a/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs +++ b/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs @@ -5,38 +5,82 @@ namespace OpenAI.Playground.TestHelpers.RealtimeHelpers; +/// +/// A comprehensive example implementation of OpenAI's Realtime API for audio interactions. +/// This class demonstrates how to: +/// - Establish and maintain a WebSocket connection with OpenAI's Realtime server +/// - Handle bidirectional audio streaming +/// - Process transcriptions and responses +/// - Implement function calling capabilities +/// - Manage the full lifecycle of a realtime conversation +/// public class RealtimeAudioExample : IDisposable { - private readonly IOpenAIRealtimeService _ai; - private readonly VoiceInput _voiceInput; - private readonly VoiceOutput _voiceOutput; + // Core services for the realtime interaction + private readonly IOpenAIRealtimeService _ai; // Manages the WebSocket connection and event handling + private readonly VoiceInput _voiceInput; // Handles audio input capture and processing + private readonly VoiceOutput _voiceOutput; // Manages audio output playback + /// + /// Initializes a new instance of the RealtimeAudioExample. + /// Sets up the necessary components for audio interaction with OpenAI's Realtime API. + /// + /// The OpenAI Realtime service instance that will manage the WebSocket connection public RealtimeAudioExample(IOpenAIRealtimeService ai) { _ai = ai; - _voiceInput = new(_ai); - _voiceOutput = new(); + _voiceInput = new(_ai); // Initialize audio input handling + _voiceOutput = new(); // Initialize audio output handling } + /// + /// Implements IDisposable to properly clean up resources. + /// This is crucial for releasing audio hardware and closing network connections. + /// public void Dispose() { - _voiceInput.Dispose(); - _voiceOutput.Dispose(); - _ai.Dispose(); + _voiceInput.Dispose(); // Release audio input resources + _voiceOutput.Dispose(); // Release audio output resources + _ai.Dispose(); // Close WebSocket connection and clean up } - + /// + /// Main execution method that orchestrates the entire realtime interaction. + /// This method: + /// 1. Sets up all necessary event handlers + /// 2. Establishes the WebSocket connection + /// 3. Configures the initial session parameters + /// 4. Handles user input for recording control + /// public async Task Run() { + // Initialize all event handlers before connecting SetupEventHandlers(); + + // Establish WebSocket connection to OpenAI's Realtime server + // This creates a new session and prepares for bi-directional communication await _ai.ConnectAsync(); + + // Configure the session with initial settings using session.update event + // This configuration defines how the AI will behave and what capabilities it has await _ai.ClientEvents.Session.Update(new() { Session = new() { + // Define the AI's personality and behavior + // This is similar to system messages in the regular Chat API Instructions = "You are a great, upbeat friend. You made jokes all the time and your voices is full of joy.", + + // Select the voice for audio responses + // Options in Realtime API: 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer' Voice = "verse", + + // Enable both text and audio capabilities + // This allows the AI to respond with both text transcriptions and spoken audio Modalities = ["text", "audio"], + + // Define tools (functions) that the AI can call during conversation + // This example implements a weather checking function Tools = [ new() @@ -44,9 +88,12 @@ await _ai.ClientEvents.Session.Update(new() Type = "function", Name = "get_current_weather", Description = "Get the current weather", + // Define the function parameters using JSON Schema Parameters = PropertyDefinition.DefineObject(new Dictionary { + // Location parameter is required { "location", PropertyDefinition.DefineString("The city and state, e.g. San Francisco, CA") }, + // Unit parameter is optional but must be either celsius or fahrenheit { "unit", PropertyDefinition.DefineEnum(["celsius", "fahrenheit"], string.Empty) } }, ["location"], null, null, null) } @@ -54,70 +101,151 @@ await _ai.ClientEvents.Session.Update(new() } }); + // Main interaction loop - Handle user commands for recording Console.WriteLine("Press 'R' to start recording, 'S' to stop, 'Q' to quit"); - while (true) { var key = Console.ReadKey(true).Key; - if (key == ConsoleKey.R) + switch (key) { - _voiceInput.StartRecording(); - Console.WriteLine("Recording started..."); - } - else if (key == ConsoleKey.S) - { - await StopAndSendAudio(); - } - else if (key == ConsoleKey.Q) - { - break; + case ConsoleKey.R: + // Start capturing audio input + _voiceInput.StartRecording(); + Console.WriteLine("Recording started..."); + break; + + case ConsoleKey.S: + // Stop recording and process the audio + await StopAndSendAudio(); + break; + + case ConsoleKey.Q: + // Exit the application + return; } } } + /// + /// Handles the process of stopping audio recording and sending it to OpenAI. + /// This method: + /// 1. Stops the audio recording + /// 2. Commits the recorded audio buffer to create a user message + /// 3. Requests an AI response + /// private async Task StopAndSendAudio() { + // Stop capturing audio input _voiceInput.StopRecording(); Console.WriteLine("Recording stopped."); + + // Commit the audio buffer to create a user message + // This triggers the input_audio_buffer.commit event await _ai.ClientEvents.InputAudioBuffer.Commit(); + + // Request an AI response for the committed audio + // This triggers the response.create event await _ai.ClientEvents.Response.Create(); } + /// + /// Utility method to send pre-recorded audio files to the API. + /// This is useful for testing or processing existing audio files. + /// + /// Path to the audio file to be sent private async Task SendPreRecordedAudio(string filePath) { Console.WriteLine($"Sending pre-recorded audio: {filePath}"); + // Send the audio file contents await _voiceInput.SendAudioFile(filePath); + // Commit the audio buffer to create a user message await _ai.ClientEvents.InputAudioBuffer.Commit(); } + /// + /// Sets up all event handlers for the realtime session. + /// This method configures handlers for: + /// - Audio input processing and transcription + /// - Speech detection + /// - AI response processing + /// - Function calls + /// - Error handling + /// + /// Each event handler corresponds to specific server events as defined in the OpenAI Realtime API documentation. + /// private void SetupEventHandlers() { - // Handle server events related to audio input - _ai.ServerEvents.Conversation.Item.InputAudioTranscription.OnCompleted += (sender, args) => { Console.WriteLine($"Transcription completed: {args.Transcript}"); }; - _ai.ServerEvents.Conversation.Item.InputAudioTranscription.OnFailed += (sender, args) => { Console.WriteLine($"Transcription failed: {args.Error}"); }; - _ai.ServerEvents.InputAudioBuffer.OnCommitted += (sender, args) => { Console.WriteLine("Audio buffer committed."); }; - _ai.ServerEvents.InputAudioBuffer.OnCleared += (sender, args) => { Console.WriteLine("Audio buffer cleared."); }; - _ai.ServerEvents.InputAudioBuffer.OnSpeechStopped += (sender, args) => { Console.WriteLine("Speech stopped detected."); }; + // AUDIO INPUT HANDLING EVENTS + + // Handle successful audio transcriptions + // This event is triggered when input audio is successfully converted to text + _ai.ServerEvents.Conversation.Item.InputAudioTranscription.OnCompleted += (sender, args) => { + Console.WriteLine($"Transcription completed: {args.Transcript}"); + }; + + // Handle failed transcription attempts + // This helps identify issues with audio quality or processing + _ai.ServerEvents.Conversation.Item.InputAudioTranscription.OnFailed += (sender, args) => { + Console.WriteLine($"Transcription failed: {args.Error}"); + }; + + // AUDIO BUFFER STATE EVENTS + + // Triggered when audio buffer is successfully committed + // This indicates the audio has been properly sent to the server + _ai.ServerEvents.InputAudioBuffer.OnCommitted += (sender, args) => { + Console.WriteLine("Audio buffer committed."); + }; + + // Triggered when audio buffer is cleared + // This happens when starting fresh or discarding unused audio + _ai.ServerEvents.InputAudioBuffer.OnCleared += (sender, args) => { + Console.WriteLine("Audio buffer cleared."); + }; + + // SPEECH DETECTION EVENTS + + // Handle speech end detection + // This helps in identifying when the user has finished speaking + _ai.ServerEvents.InputAudioBuffer.OnSpeechStopped += (sender, args) => { + Console.WriteLine("Speech stopped detected."); + }; + + // Handle speech start detection + // This is useful for implementing real-time interaction _ai.ServerEvents.InputAudioBuffer.OnSpeechStarted += async (sender, args) => { Console.WriteLine("Speech started detected."); + // Clear any ongoing audio output when user starts speaking _voiceOutput.StopAndClear(); - // Optionally, notify the server to cancel any ongoing responses + // Cancel any in-progress AI responses + // This ensures a more natural conversation flow await _ai.ClientEvents.Response.Cancel(); }; + + // AI RESPONSE HANDLING EVENTS + + // Handle incoming text transcripts from the AI + // This shows what the AI is saying in text form _ai.ServerEvents.Response.AudioTranscript.OnDelta += (sender, args) => { Console.ForegroundColor = ConsoleColor.DarkGreen; Console.Write($"{args.Delta}"); Console.ResetColor(); }; + + // AUDIO OUTPUT HANDLING + + // Process incoming audio data from the AI + // This handles the AI's voice response in chunks _ai.ServerEvents.Response.Audio.OnDelta += (sender, args) => { try { if (!string.IsNullOrEmpty(args.Delta)) { + // Convert base64 audio data to bytes and queue for playback var audioData = Convert.FromBase64String(args.Delta); _voiceOutput.EnqueueAudioData(audioData); } @@ -127,13 +255,18 @@ private void SetupEventHandlers() Console.WriteLine($"Error processing audio delta: {ex.Message}"); } }; + + // Handle completion of audio response _ai.ServerEvents.Response.Audio.OnDone += (sender, args) => { Console.WriteLine(); Console.WriteLine("Audio response completed."); }; + // FUNCTION CALLING EVENTS + // Handle incoming function call arguments + // This shows the AI's attempts to use tools/functions _ai.ServerEvents.Response.FunctionCallArguments.OnDelta += (sender, args) => { Console.ForegroundColor = ConsoleColor.Yellow; @@ -141,28 +274,47 @@ private void SetupEventHandlers() Console.ResetColor(); }; + // Process completed function calls _ai.ServerEvents.Response.FunctionCallArguments.OnDone += async (sender, args) => { if (args.Arguments != null) { Console.WriteLine($"Function call completed: {args.Arguments}"); + // Handle weather function calls specifically if (args.Name == "get_current_weather") { await HandleWeatherFunction(args.Arguments, args.CallId); } } }; - _ai.ServerEvents.OnError += (sender, args) => { Console.WriteLine($"Error: {args.Error.Message}"); }; - //for debug - //_ai.ServerEvents.OnAll += (sender, args) => { Console.WriteLine($"Received response: {args}"); }; + + // ERROR HANDLING + + // Global error handler for any API errors + _ai.ServerEvents.OnError += (sender, args) => { + Console.WriteLine($"Error: {args.Error.Message}"); + }; } + /// + /// Handles weather function calls from the AI. + /// This method: + /// 1. Parses the function arguments + /// 2. Simulates a weather API call + /// 3. Returns the results to the AI + /// 4. Triggers a new response based on the weather data + /// + /// JSON string containing the function arguments + /// Unique identifier for the function call private async Task HandleWeatherFunction(string arguments, string callId) { try { + // Parse the weather query arguments var args = JsonSerializer.Deserialize(arguments); - // Simulate weather API call + + // Simulate getting weather data + // In a real application, this would call an actual weather API var weatherResult = new { temperature = args.unit == "celsius" ? 22 : 72, @@ -171,7 +323,8 @@ private async Task HandleWeatherFunction(string arguments, string callId) location = args.location }; - // Create function output + // Send the weather data back to the conversation + // This creates a function_call_output item in the conversation await _ai.ClientEvents.Conversation.Item.Create(new() { Item = new() @@ -182,7 +335,7 @@ await _ai.ClientEvents.Conversation.Item.Create(new() } }); - // Generate new response + // Request a new AI response based on the weather data await _ai.ClientEvents.Response.Create(); } catch (Exception ex) @@ -191,9 +344,13 @@ await _ai.ClientEvents.Conversation.Item.Create(new() } } + /// + /// Data model for weather function arguments. + /// This class maps to the JSON schema defined in the function parameters. + /// private class WeatherArgs { - public string location { get; set; } - public string unit { get; set; } + public string location { get; set; } // Required: city and state + public string unit { get; set; } // Optional: celsius or fahrenheit } } \ No newline at end of file diff --git a/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs index 323bb0a1..438958ea 100644 --- a/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs +++ b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceInput.cs @@ -3,31 +3,58 @@ namespace OpenAI.Playground.TestHelpers.RealtimeHelpers; +/// +/// Handles voice input capture and processing for real-time communication with OpenAI's API. +/// This class manages audio recording, buffering, and transmission of audio data. +/// public class VoiceInput : IDisposable { + // Minimum amount of audio to buffer before sending (in milliseconds) private const int MinimumBufferMs = 100; + + // Buffer to store audio data before sending private readonly List _audioBuffer; + + // Reference to the OpenAI real-time service client private readonly IOpenAIRealtimeService _client; + + // NAudio's wave input device for capturing audio private readonly WaveInEvent _waveIn; + + // Flag to track recording state private bool _isRecording; + /// + /// Initializes a new instance of VoiceInput with specified OpenAI client. + /// + /// The OpenAI real-time service client public VoiceInput(IOpenAIRealtimeService client) { _client = client; + // Configure audio input with specific format: + // - 24000 Hz sample rate + // - 16 bits per sample + // - 1 channel (mono) _waveIn = new() { WaveFormat = new(24000, 16, 1), - BufferMilliseconds = 50 + BufferMilliseconds = 50 // How often to receive audio data }; _audioBuffer = []; _waveIn.DataAvailable += OnDataAvailable!; } + /// + /// Releases resources used by the voice input system + /// public void Dispose() { _waveIn.Dispose(); } + /// + /// Starts recording audio from the default input device + /// public void StartRecording() { if (_isRecording) return; @@ -36,13 +63,16 @@ public void StartRecording() _waveIn.StartRecording(); } + /// + /// Stops recording audio and sends any remaining buffered data + /// public void StopRecording() { if (!_isRecording) return; _isRecording = false; _waveIn.StopRecording(); - // Send any remaining buffered audio + // Send any remaining buffered audio before stopping if (_audioBuffer.Count > 0) { _client.ClientEvents.InputAudioBuffer.Append(_audioBuffer.ToArray()); @@ -50,6 +80,9 @@ public void StopRecording() } } + /// + /// Handles incoming audio data from the recording device + /// private void OnDataAvailable(object sender, WaveInEventArgs e) { if (!_isRecording) return; @@ -57,10 +90,10 @@ private void OnDataAvailable(object sender, WaveInEventArgs e) // Add new audio data to the buffer _audioBuffer.AddRange(e.Buffer.Take(e.BytesRecorded)); - // Calculate buffer duration in milliseconds + // Calculate current buffer duration in milliseconds var bufferDurationMs = _audioBuffer.Count * 1000.0 / _waveIn.WaveFormat.AverageBytesPerSecond; - // Only send when we have at least MinimumBufferMs of audio + // Only send when we have accumulated enough audio data if (bufferDurationMs >= MinimumBufferMs) { _client.ClientEvents.InputAudioBuffer.Append(_audioBuffer.ToArray()); @@ -68,46 +101,66 @@ private void OnDataAvailable(object sender, WaveInEventArgs e) } } + /// + /// Sends an audio file to the OpenAI API by streaming it in chunks + /// + /// Path to the audio file to send public async Task SendAudioFile(string filePath) { using var audioFileReader = new AudioFileReader(filePath); + // Calculate buffer size based on minimum buffer duration var bufferSize = (int)(audioFileReader.WaveFormat.AverageBytesPerSecond * (MinimumBufferMs / 1000.0)); var buffer = new byte[bufferSize]; int bytesRead; + // Read and send the file in chunks while ((bytesRead = await audioFileReader.ReadAsync(buffer, 0, buffer.Length)) > 0) { if (bytesRead < buffer.Length) { - // Handle the last partial buffer + // Handle the last chunk if it's smaller than the buffer var lastBuffer = new byte[bytesRead]; Array.Copy(buffer, lastBuffer, bytesRead); buffer = lastBuffer; } + // Resample the audio to match required format and send var resampledBuffer = ResampleAudio(buffer, bytesRead, audioFileReader.WaveFormat, _waveIn.WaveFormat); await _client.ClientEvents.InputAudioBuffer.Append(resampledBuffer); } } + /// + /// Resamples audio data to match the target format required by the API + /// + /// Original audio data + /// Number of bytes in the buffer + /// Original audio format + /// Desired output format + /// Resampled audio data private static byte[] ResampleAudio(byte[] buffer, int bytesRead, WaveFormat sourceFormat, WaveFormat targetFormat) { - if (sourceFormat.SampleRate == targetFormat.SampleRate && sourceFormat.BitsPerSample == targetFormat.BitsPerSample && sourceFormat.Channels == targetFormat.Channels) + // Skip resampling if formats match + if (sourceFormat.SampleRate == targetFormat.SampleRate && + sourceFormat.BitsPerSample == targetFormat.BitsPerSample && + sourceFormat.Channels == targetFormat.Channels) { - // No resampling needed var trimmedBuffer = new byte[bytesRead]; Array.Copy(buffer, trimmedBuffer, bytesRead); return trimmedBuffer; } + // Perform resampling using MediaFoundation using var sourceStream = new RawSourceWaveStream(buffer, 0, bytesRead, sourceFormat); using var resampler = new MediaFoundationResampler(sourceStream, targetFormat); - resampler.ResamplerQuality = 60; + resampler.ResamplerQuality = 60; // Set high quality resampling + // Calculate and allocate buffer for resampled audio var resampledBytes = (int)(bytesRead * ((double)targetFormat.AverageBytesPerSecond / sourceFormat.AverageBytesPerSecond)); var resampledBuffer = new byte[resampledBytes]; var resampledBytesRead = resampler.Read(resampledBuffer, 0, resampledBytes); + // Trim the buffer to actual size and return var trimmedBuffer2 = new byte[resampledBytesRead]; Array.Copy(resampledBuffer, trimmedBuffer2, resampledBytesRead); return trimmedBuffer2; diff --git a/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs index a58fae29..a4d22584 100644 --- a/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs +++ b/OpenAI.Playground/TestHelpers/RealtimeHelpers/VoiceOutput.cs @@ -3,37 +3,67 @@ namespace OpenAI.Playground.TestHelpers.RealtimeHelpers; +/// +/// Handles real-time audio playback for OpenAI's audio responses +/// Manages buffering and streaming of audio data +/// public class VoiceOutput : IDisposable { - private readonly BufferedWaveProvider _bufferedWaveProvider; - private readonly WaveOutEvent _waveOut; - private bool _isPlaying; + // Core components for audio handling + private readonly BufferedWaveProvider _bufferedWaveProvider; // Manages audio data buffering + private readonly WaveOutEvent _waveOut; // Handles audio output device + private bool _isPlaying; // Tracks current playback status + /// + /// Initializes the voice output system with OpenAI's default audio settings + /// public VoiceOutput() { + // Initialize audio output device _waveOut = new(); + // Register for playback stopped events _waveOut.PlaybackStopped += OnPlaybackStopped!; - _bufferedWaveProvider = new(new(RealtimeConstants.Audio.DefaultSampleRate, RealtimeConstants.Audio.DefaultBitsPerSample, RealtimeConstants.Audio.DefaultChannels)) + + // Configure audio buffer with OpenAI's default settings + _bufferedWaveProvider = new(new( + RealtimeConstants.Audio.DefaultSampleRate, // Standard sample rate + RealtimeConstants.Audio.DefaultBitsPerSample, // Bit depth for audio + RealtimeConstants.Audio.DefaultChannels // Number of audio channels + )) { - BufferLength = 10 * 1024 * 1024, // 10 MB buffer - increase if needed - DiscardOnBufferOverflow = true + BufferLength = 10 * 1024 * 1024, // Set 10 MB buffer size for smooth playback + DiscardOnBufferOverflow = true // Prevent buffer overflow by discarding excess data }; + + // Connect the buffer to the audio output _waveOut.Init(_bufferedWaveProvider); } + /// + /// Cleanup resources when object is disposed + /// public void Dispose() { + // Stop playback and release audio device resources _waveOut.Stop(); _waveOut.Dispose(); } + /// + /// Add new audio data to the playback queue + /// Automatically starts playback if not already playing + /// + /// Raw audio data bytes to be played public void EnqueueAudioData(byte[]? data) { + // Ignore empty or null data if (data == null || data.Length == 0) return; + // Add new audio data to the buffer _bufferedWaveProvider.AddSamples(data, 0, data.Length); + // Start playback if not already playing if (!_isPlaying) { _waveOut.Play(); @@ -41,24 +71,35 @@ public void EnqueueAudioData(byte[]? data) } } + /// + /// Stops playback and clears any remaining buffered audio + /// public void StopAndClear() { + // Stop playback if currently playing if (_isPlaying) { _waveOut.Stop(); _isPlaying = false; } + // Clear any remaining audio from buffer _bufferedWaveProvider.ClearBuffer(); Console.WriteLine("Playback stopped and buffer cleared."); } + /// + /// Event handler for when playback stops + /// Restarts playback if there's more data in buffer + /// private void OnPlaybackStopped(object sender, StoppedEventArgs e) { + // If there's more audio in the buffer, continue playing if (_bufferedWaveProvider.BufferedBytes > 0) { _waveOut.Play(); } + // Otherwise, mark playback as stopped else { _isPlaying = false; diff --git a/OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs b/OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs index d1687a9b..39bca04f 100644 --- a/OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs +++ b/OpenAI.SDK/Builders/OpenAIRealtimeServiceBuilder.cs @@ -8,6 +8,11 @@ namespace Betalgo.Ranul.OpenAI.Builders; +/// +/// Builder class for configuring and creating OpenAI Realtime WebSocket services. +/// Provides functionality to establish WebSocket connections for real-time communication +/// with OpenAI's GPT models, supporting both audio and text modalities. +/// public class OpenAIRealtimeServiceBuilder { private readonly Dictionary _headers = new(); @@ -18,65 +23,120 @@ public class OpenAIRealtimeServiceBuilder private ILogger? _logger; private ServiceLifetime _serviceLifetime = ServiceLifetime.Singleton; - // Constructors for standalone and DI scenarios + /// + /// Initializes a new instance of the OpenAIRealtimeServiceBuilder with an API key. + /// + /// The OpenAI API key used for authentication. public OpenAIRealtimeServiceBuilder(string apiKey) : this(new OpenAIOptions { ApiKey = apiKey }) { } + /// + /// Initializes a new instance of the OpenAIRealtimeServiceBuilder with custom options and optional logger. + /// + /// The OpenAI configuration options. + /// Optional logger for service diagnostics. public OpenAIRealtimeServiceBuilder(OpenAIOptions options, ILogger? logger = null) { _options = options; _logger = logger; } + /// + /// Initializes a new instance of the OpenAIRealtimeServiceBuilder for dependency injection scenarios. + /// + /// The service collection for dependency injection. + /// The service lifetime (Singleton by default). public OpenAIRealtimeServiceBuilder(IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Singleton) { _services = services; _serviceLifetime = lifetime; } + /// + /// Configures the WebSocket instance after creation. + /// + /// Action to configure the WebSocket. + /// The builder instance for method chaining. public OpenAIRealtimeServiceBuilder ConfigureWebSocket(Action configure) { _configureWebSocket = configure; return this; } + /// + /// Configures the WebSocket options before connection. + /// + /// Action to configure the WebSocket options. + /// The builder instance for method chaining. public OpenAIRealtimeServiceBuilder ConfigureOptions(Action configure) { _configureOptions = configure; return this; } + /// + /// Adds a custom header to the WebSocket connection request. + /// + /// The header name. + /// The header value. + /// The builder instance for method chaining. public OpenAIRealtimeServiceBuilder AddHeader(string name, string value) { _headers[name] = value; return this; } + /// + /// Sets the logger for the service. + /// + /// The logger instance. + /// The builder instance for method chaining. public OpenAIRealtimeServiceBuilder WithLogger(ILogger logger) { _logger = logger; return this; } + /// + /// Sets the service lifetime to Singleton. + /// + /// The builder instance for method chaining. public OpenAIRealtimeServiceBuilder AsSingleton() { _serviceLifetime = ServiceLifetime.Singleton; return this; } + /// + /// Sets the service lifetime to Scoped. + /// + /// The builder instance for method chaining. public OpenAIRealtimeServiceBuilder AsScoped() { _serviceLifetime = ServiceLifetime.Scoped; return this; } + /// + /// Sets the service lifetime to Transient. + /// + /// The builder instance for method chaining. public OpenAIRealtimeServiceBuilder AsTransient() { _serviceLifetime = ServiceLifetime.Transient; return this; } + /// + /// Builds and returns an instance of IOpenAIRealtimeService. + /// When used with dependency injection, registers the service with the specified lifetime + /// and returns null. When used standalone, returns the configured service instance. + /// + /// + /// The configured IOpenAIRealtimeService instance for standalone usage, + /// or null when used with dependency injection. + /// public IOpenAIRealtimeService? Build() { if (_services == null) @@ -131,8 +191,19 @@ private void ConfigureClient(OpenAIWebSocketClient client) } } +/// +/// Helper class for configuring WebSocket connections with OpenAI Realtime API settings. +/// public static class WebSocketConfigurationHelper { + /// + /// Configures a WebSocket instance with OpenAI Realtime API specific settings. + /// Sets up required subprotocols and applies custom configurations. + /// + /// The WebSocket instance to configure. + /// Optional action to configure WebSocket options. + /// Optional dictionary of custom headers to add to the connection. + /// Optional action to perform additional WebSocket configuration. public static void ConfigureWebSocket(ClientWebSocket webSocket, Action? configureOptions = null, IReadOnlyDictionary? headers = null, Action? configureWebSocket = null) { try diff --git a/OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs b/OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs index 968b7de1..5c791da8 100644 --- a/OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs +++ b/OpenAI.SDK/Extensions/OpenAIRealtimeServiceCollectionExtensions.cs @@ -4,18 +4,44 @@ namespace Betalgo.Ranul.OpenAI.Extensions; +/// +/// Contains extension methods for configuring OpenAI Realtime services in an ASP.NET Core application. +/// +/// +/// The OpenAI Realtime service enables real-time communication with a GPT-4 class model over WebSocket, +/// supporting both audio and text transcriptions. +/// public static class OpenAIRealtimeServiceCollectionExtensions { + /// + /// Adds OpenAI Realtime services to the specified using configuration-based setup. + /// + /// The to add services to. + /// An that can be used to further configure the OpenAI Realtime services. + /// + /// This method configures the OpenAI Realtime service using the application's configuration system, + /// looking for settings under the OpenAIOptions.SettingKey section. + /// public static OpenAIRealtimeServiceBuilder AddOpenAIRealtimeService(this IServiceCollection services) { var optionsBuilder = services.AddOptions(); optionsBuilder.BindConfiguration(OpenAIOptions.SettingKey); - var builder = new OpenAIRealtimeServiceBuilder(services); builder.Build(); return builder; } + /// + /// Adds OpenAI Realtime services to the specified using a configuration action. + /// + /// The to add services to. + /// A delegate to configure the . + /// An that can be used to further configure the OpenAI Realtime services. + /// + /// This method allows direct configuration of OpenAI Realtime options through a delegate, + /// providing programmatic control over service settings such as modalities, voice settings, + /// and audio format configurations. + /// public static OpenAIRealtimeServiceBuilder AddOpenAIRealtimeService(this IServiceCollection services, Action configureOptions) { services.Configure(configureOptions); @@ -24,10 +50,25 @@ public static OpenAIRealtimeServiceBuilder AddOpenAIRealtimeService(this IServic return builder; } + /// + /// Adds OpenAI Realtime services to the specified using an section. + /// + /// The to add services to. + /// The configuration section containing OpenAI Realtime settings. + /// An that can be used to further configure the OpenAI Realtime services. + /// + /// This method configures the OpenAI Realtime service using a specific configuration section, + /// allowing for flexible configuration management through different configuration providers. + /// The configuration can include settings for: + /// - Audio input/output formats (pcm16, g711_ulaw, g711_alaw) + /// - Voice selection (alloy, echo, shimmer) + /// - Modalities (text, audio) + /// - Turn detection settings + /// - Audio transcription options + /// public static OpenAIRealtimeServiceBuilder AddOpenAIRealtimeService(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection(OpenAIOptions.SettingKey)); - var builder = new OpenAIRealtimeServiceBuilder(services); builder.Build(); return builder; diff --git a/OpenAI.SDK/Managers/OpenAIRealtimeService.cs b/OpenAI.SDK/Managers/OpenAIRealtimeService.cs index b47b8fa6..08ade525 100644 --- a/OpenAI.SDK/Managers/OpenAIRealtimeService.cs +++ b/OpenAI.SDK/Managers/OpenAIRealtimeService.cs @@ -15,18 +15,79 @@ namespace Betalgo.Ranul.OpenAI.Managers; +/// +/// WebSocket client wrapper for OpenAI Realtime API connections. +/// public class OpenAIWebSocketClient { + /// + /// Gets the underlying WebSocket client instance. + /// public ClientWebSocket WebSocket { get; } = new(); + /// + /// Configures the WebSocket client with custom settings. + /// + /// Action to configure the WebSocket client. public void ConfigureWebSocket(Action configure) { configure(WebSocket); } } +/// +/// Service interface for interacting with the OpenAI Realtime API over WebSocket. +/// Provides real-time communication capabilities for text and audio interactions. +/// +public interface IOpenAIRealtimeService : IDisposable, IAsyncDisposable +{ + /// + /// Gets a value indicating whether the service is currently connected to the OpenAI Realtime API. + /// + /// True if connected to the WebSocket server, otherwise false. + bool IsConnected { get; } + + /// + /// Gets the client events interface for sending events to the OpenAI Realtime API. + /// These events include session updates, audio buffer operations, conversation management, and response generation. + /// + IOpenAIRealtimeServiceClientEvents ClientEvents { get; } + + /// + /// Gets the server events interface for receiving events from the OpenAI Realtime API. + /// These events include status updates, content streaming, and error notifications. + /// + IOpenAIRealtimeServiceServerEvents ServerEvents { get; } + /// + /// Establishes a WebSocket connection to the OpenAI Realtime API. + /// + /// Optional cancellation token to cancel the connection attempt. + /// A task that represents the asynchronous connection operation. + /// Thrown when already connected or when connection fails. + /// Thrown when the operation is canceled. + Task ConnectAsync(CancellationToken cancellationToken = default); + + /// + /// Gracefully closes the WebSocket connection to the OpenAI Realtime API. + /// + /// Optional cancellation token to cancel the disconnection attempt. + /// A task that represents the asynchronous disconnection operation. + /// Thrown when the operation is canceled. + Task DisconnectAsync(CancellationToken cancellationToken = default); +} + +/// +/// Main implementation of the OpenAI Realtime service providing WebSocket-based communication. +/// Supports real-time text and audio interactions with GPT-4 and related models. +/// public partial class OpenAIRealtimeService : IOpenAIRealtimeService { + /// + /// Initializes a new instance of the OpenAIRealtimeService with dependency injection support. + /// + /// OpenAI API configuration options. + /// Logger instance for service diagnostics. + /// WebSocket client for API communication. public OpenAIRealtimeService(IOptions settings, ILogger logger, OpenAIWebSocketClient webSocketClient) { _openAIOptions = settings.Value; @@ -37,14 +98,28 @@ public OpenAIRealtimeService(IOptions settings, ILogger + /// Initializes a new instance of the OpenAIRealtimeService with minimal configuration. + /// + /// OpenAI API configuration options. public OpenAIRealtimeService(OpenAIOptions options) : this(Options.Create(options), NullLogger.Instance, new()) { } + /// + /// Initializes a new instance of the OpenAIRealtimeService with logging support. + /// + /// OpenAI API configuration options. + /// Logger instance for service diagnostics. public OpenAIRealtimeService(OpenAIOptions options, ILogger logger) : this(Options.Create(options), logger, new()) { } + /// + /// Initializes a new instance of the OpenAIRealtimeService with custom WebSocket configuration. + /// + /// OpenAI API configuration options. + /// Optional action to configure the WebSocket client. public OpenAIRealtimeService(OpenAIOptions options, Action? configureWebSocket = null) : this(options) { if (configureWebSocket != null) @@ -53,21 +128,37 @@ public OpenAIRealtimeService(OpenAIOptions options, Action? con } } + /// + /// Initializes a new instance of the OpenAIRealtimeService with just an API key. + /// + /// OpenAI API key for authentication. public OpenAIRealtimeService(string apiKey) : this(new OpenAIOptions { ApiKey = apiKey }) { } + /// + /// Creates a new OpenAIRealtimeService builder instance using an API key. + /// + /// OpenAI API key for authentication. + /// A builder instance for configuring the service. public static OpenAIRealtimeServiceBuilder Create(string apiKey) { return new(apiKey); } + /// + /// Creates a new OpenAIRealtimeService builder instance using configuration options. + /// + /// OpenAI API configuration options. + /// A builder instance for configuring the service. public static OpenAIRealtimeServiceBuilder Create(OpenAIOptions options) { return new(options); } - + /// + /// Configures the base WebSocket connection with authentication headers. + /// private void ConfigureBaseWebSocket() { var headers = new Dictionary @@ -82,8 +173,10 @@ private void ConfigureBaseWebSocket() _webSocketClient.ConfigureWebSocket(ws => WebSocketConfigurationHelper.ConfigureWebSocket(ws, headers: headers)); } + } + public partial class OpenAIRealtimeService : IOpenAIRealtimeService { #if !NET6_0_OR_GREATER @@ -489,16 +582,3 @@ public async ValueTask DisposeAsync() _disposeCts.Dispose(); } } - -public interface IOpenAIRealtimeService : IDisposable, IAsyncDisposable -{ - bool IsConnected { get; } - - IOpenAIRealtimeServiceClientEvents ClientEvents { get; } - - IOpenAIRealtimeServiceServerEvents ServerEvents { get; } - - Task ConnectAsync(CancellationToken cancellationToken = default); - - Task DisconnectAsync(CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs b/OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs index b6c284f5..a9f311dc 100644 --- a/OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs +++ b/OpenAI.SDK/Managers/OpenAIRealtimeServiceClientEvents.cs @@ -135,41 +135,142 @@ public Task Cancel(CancellationToken cancellationToken = default) } } + +/// +/// Interface for interacting with the OpenAI Realtime WebSocket server client events. +/// public interface IOpenAIRealtimeServiceClientEvents { + /// + /// Provides access to session-related operations. + /// ISession Session { get; } + + /// + /// Provides access to input audio buffer operations. + /// IInputAudioBuffer InputAudioBuffer { get; } + + /// + /// Provides access to conversation-related operations. + /// IConversation Conversation { get; } + + /// + /// Provides access to response-related operations. + /// IResponse Response { get; } + /// + /// Interface for managing session configuration. + /// public interface ISession { + /// + /// Updates the session's default configuration. + /// + /// Configuration settings including modalities, instructions, voice, audio formats, tools, and more. + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Update(SessionUpdateRequest request, CancellationToken cancellationToken = default); } + /// + /// Interface for managing input audio buffer operations. + /// public interface IInputAudioBuffer { + /// + /// Appends audio bytes to the input audio buffer. + /// + /// The audio data to append. + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Append(ReadOnlyMemory audioData, CancellationToken cancellationToken = default); + + /// + /// Commits audio bytes to a user message. + /// + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Commit(CancellationToken cancellationToken = default); + + /// + /// Clears the audio bytes in the buffer. + /// + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Clear(CancellationToken cancellationToken = default); } + /// + /// Interface for managing conversation operations. + /// public interface IConversation { + /// + /// Provides access to conversation item operations. + /// IItem Item { get; } + /// + /// Interface for managing conversation items. + /// public interface IItem { + /// + /// Creates a new item in the conversation. + /// + /// Request containing item details such as type, status, role, and content. + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Create(ConversationItemCreateRequest request, CancellationToken cancellationToken = default); + + /// + /// Truncates a previous assistant message's audio. + /// + /// The ID of the assistant message item to truncate. + /// The index of the content part to truncate. + /// Inclusive duration up to which audio is truncated, in milliseconds. + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Truncate(string itemId, int contentIndex, int audioEndMs, CancellationToken cancellationToken = default); + + /// + /// Removes any item from the conversation history. + /// + /// The ID of the item to delete. + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Delete(string itemId, CancellationToken cancellationToken = default); } } + /// + /// Interface for managing response operations. + /// public interface IResponse { + /// + /// Triggers a response generation with specific configuration. + /// + /// Configuration for the response including modalities, instructions, voice, tools, and other settings. + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Create(ResponseCreateRequest request, CancellationToken cancellationToken = default); + + /// + /// Triggers a response generation with default configuration. + /// + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Create(CancellationToken cancellationToken = default); + + /// + /// Cancels an in-progress response. + /// + /// Optional cancellation token. + /// A task representing the asynchronous operation. Task Cancel(CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs b/OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs index 1597b541..0116eadb 100644 --- a/OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs +++ b/OpenAI.SDK/Managers/OpenAIRealtimeServiceServerEvents.cs @@ -2,103 +2,6 @@ namespace Betalgo.Ranul.OpenAI.Managers; -public interface IOpenAIRealtimeServiceServerEvents -{ - ISession Session { get; } - IConversation Conversation { get; } - IInputAudioBuffer InputAudioBuffer { get; } - IResponse Response { get; } - IRateLimits RateLimits { get; } - event EventHandler? OnError; - event EventHandler? OnAll; - - public interface ISession - { - event EventHandler? OnCreated; - event EventHandler? OnUpdated; - } - - public interface IConversation - { - IItem Item { get; } - event EventHandler? OnCreated; - - public interface IItem - { - IInputAudioTranscription InputAudioTranscription { get; } - event EventHandler? OnCreated; - event EventHandler? OnTruncated; - event EventHandler? OnDeleted; - - public interface IInputAudioTranscription - { - event EventHandler? OnCompleted; - event EventHandler? OnFailed; - } - } - } - - public interface IInputAudioBuffer - { - event EventHandler? OnCommitted; - event EventHandler? OnCleared; - event EventHandler? OnSpeechStarted; - event EventHandler? OnSpeechStopped; - } - - public interface IResponse - { - IOutputItem OutputItem { get; } - IContentPart ContentPart { get; } - IText Text { get; } - IAudioTranscript AudioTranscript { get; } - IAudio Audio { get; } - IFunctionCallArguments FunctionCallArguments { get; } - event EventHandler? OnCreated; - event EventHandler? OnDone; - - public interface IOutputItem - { - event EventHandler? OnAdded; - event EventHandler? OnDone; - } - - public interface IContentPart - { - event EventHandler? OnAdded; - event EventHandler? OnDone; - } - - public interface IText - { - event EventHandler? OnDelta; - event EventHandler? OnDone; - } - - public interface IAudioTranscript - { - event EventHandler? OnDelta; - event EventHandler? OnDone; - } - - public interface IAudio - { - event EventHandler? OnDelta; - event EventHandler? OnDone; - } - - public interface IFunctionCallArguments - { - event EventHandler? OnDelta; - event EventHandler? OnDone; - } - } - - public interface IRateLimits - { - event EventHandler? OnUpdated; - } -} public class OpenAIRealtimeServiceServerEvents : IOpenAIRealtimeServiceServerEvents { @@ -358,4 +261,307 @@ internal void RaiseOnUpdated(object sender, RateLimitsEvent e) OnUpdated?.Invoke(sender, e); } } +} + + + + +/// +/// Interface for handling OpenAI Realtime WebSocket server events. +/// Provides access to various components of the realtime communication system. +/// +public interface IOpenAIRealtimeServiceServerEvents +{ + /// + /// Provides access to session-related events and functionality. + /// + ISession Session { get; } + + /// + /// Provides access to conversation-related events and functionality. + /// + IConversation Conversation { get; } + + /// + /// Provides access to input audio buffer events and functionality. + /// + IInputAudioBuffer InputAudioBuffer { get; } + + /// + /// Provides access to response-related events and functionality. + /// + IResponse Response { get; } + + /// + /// Provides access to rate limits events and functionality. + /// + IRateLimits RateLimits { get; } + + /// + /// Event raised when an error occurs during realtime communication. + /// + event EventHandler? OnError; + + /// + /// Event raised for all server events, providing raw event data. + /// + event EventHandler? OnAll; + + /// + /// Interface for handling session-related events. + /// + public interface ISession + { + /// + /// Event raised when a new session is created. Emitted automatically when a new connection is established. + /// + event EventHandler? OnCreated; + + /// + /// Event raised when a session is updated with new configuration. + /// + event EventHandler? OnUpdated; + } + + /// + /// Interface for handling conversation-related events. + /// + public interface IConversation + { + /// + /// Provides access to conversation item-related events. + /// + IItem Item { get; } + + /// + /// Event raised when a conversation is created. Emitted right after session creation. + /// + event EventHandler? OnCreated; + + /// + /// Interface for handling conversation item events. + /// + public interface IItem + { + /// + /// Provides access to input audio transcription events. + /// + IInputAudioTranscription InputAudioTranscription { get; } + + /// + /// Event raised when a conversation item is created. + /// + event EventHandler? OnCreated; + + /// + /// Event raised when an earlier assistant audio message item is truncated by the client. + /// + event EventHandler? OnTruncated; + + /// + /// Event raised when an item in the conversation is deleted. + /// + event EventHandler? OnDeleted; + + /// + /// Interface for handling input audio transcription events. + /// + public interface IInputAudioTranscription + { + /// + /// Event raised when input audio transcription is enabled and a transcription succeeds. + /// + event EventHandler? OnCompleted; + + /// + /// Event raised when input audio transcription is configured, and a transcription request for a user message failed. + /// + event EventHandler? OnFailed; + } + } + } + + /// + /// Interface for handling input audio buffer events. + /// + public interface IInputAudioBuffer + { + /// + /// Event raised when an input audio buffer is committed, either by the client or automatically in server VAD mode. + /// + event EventHandler? OnCommitted; + + /// + /// Event raised when the input audio buffer is cleared by the client. + /// + event EventHandler? OnCleared; + + /// + /// Event raised in server turn detection mode when speech is detected. + /// + event EventHandler? OnSpeechStarted; + + /// + /// Event raised in server turn detection mode when speech stops. + /// + event EventHandler? OnSpeechStopped; + } + + /// + /// Interface for handling response-related events. + /// + public interface IResponse + { + /// + /// Provides access to response output item events. + /// + IOutputItem OutputItem { get; } + + /// + /// Provides access to content part events. + /// + IContentPart ContentPart { get; } + + /// + /// Provides access to text stream events. + /// + IText Text { get; } + + /// + /// Provides access to audio transcript stream events. + /// + IAudioTranscript AudioTranscript { get; } + + /// + /// Provides access to audio stream events. + /// + IAudio Audio { get; } + + /// + /// Provides access to function call arguments stream events. + /// + IFunctionCallArguments FunctionCallArguments { get; } + + /// + /// Event raised when a new Response is created. The first event of response creation, where the response is in an initial state of "in_progress". + /// + event EventHandler? OnCreated; + + /// + /// Event raised when a Response is done streaming. Always emitted, no matter the final state. + /// + event EventHandler? OnDone; + + /// + /// Interface for handling response output item events. + /// + public interface IOutputItem + { + /// + /// Event raised when a new Item is created during response generation. + /// + event EventHandler? OnAdded; + + /// + /// Event raised when an Item is done streaming. Also emitted when a Response is interrupted, incomplete, or cancelled. + /// + event EventHandler? OnDone; + } + + /// + /// Interface for handling content part events. + /// + public interface IContentPart + { + /// + /// Event raised when a new content part is added to an assistant message item during response generation. + /// + event EventHandler? OnAdded; + + /// + /// Event raised when a content part is done streaming in an assistant message item. + /// Also emitted when a Response is interrupted, incomplete, or cancelled. + /// + event EventHandler? OnDone; + } + + /// + /// Interface for handling text stream events. + /// + public interface IText + { + /// + /// Event raised when the text value of a "text" content part is updated. + /// + event EventHandler? OnDelta; + + /// + /// Event raised when the text value of a "text" content part is done streaming. + /// Also emitted when a Response is interrupted, incomplete, or cancelled. + /// + event EventHandler? OnDone; + } + + /// + /// Interface for handling audio transcript stream events. + /// + public interface IAudioTranscript + { + /// + /// Event raised when the model-generated transcription of audio output is updated. + /// + event EventHandler? OnDelta; + + /// + /// Event raised when the model-generated transcription of audio output is done streaming. + /// Also emitted when a Response is interrupted, incomplete, or cancelled. + /// + event EventHandler? OnDone; + } + + /// + /// Interface for handling audio stream events. + /// + public interface IAudio + { + /// + /// Event raised when the model-generated audio is updated. + /// + event EventHandler? OnDelta; + + /// + /// Event raised when the model-generated audio is done. + /// Also emitted when a Response is interrupted, incomplete, or cancelled. + /// + event EventHandler? OnDone; + } + + /// + /// Interface for handling function call arguments stream events. + /// + public interface IFunctionCallArguments + { + /// + /// Event raised when the model-generated function call arguments are updated. + /// + event EventHandler? OnDelta; + + /// + /// Event raised when the model-generated function call arguments are done streaming. + /// Also emitted when a Response is interrupted, incomplete, or cancelled. + /// + event EventHandler? OnDone; + } + } + + /// + /// Interface for handling rate limits events. + /// + public interface IRateLimits + { + /// + /// Event raised after every "response.done" event to indicate the updated rate limits. + /// + event EventHandler? OnUpdated; + } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs index 317eed20..23c6b2a4 100644 --- a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeConstants.cs @@ -1,50 +1,115 @@ namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; /// -/// Contains constants used by the Realtime API implementation. +/// Contains constants used by the OpenAI Realtime API implementation for real-time communication +/// with GPT-4 class models over WebSocket, supporting both audio and text transcriptions. /// public static class RealtimeConstants { /// - /// WebSocket subprotocols used for connection. + /// WebSocket subprotocols used for establishing connection with the OpenAI Realtime WebSocket server. /// public static class SubProtocols { + /// + /// The primary realtime subprotocol identifier. + /// public const string Realtime = "realtime"; + + /// + /// The beta version subprotocol identifier for OpenAI realtime API. + /// public const string Beta = "openai-beta.realtime-v1"; } /// - /// Headers used in API requests. + /// Standard headers required for authentication and configuration in API requests. /// public static class Headers { + /// + /// The authorization header for API authentication. + /// public const string Authorization = "Authorization"; + + /// + /// Header indicating beta feature usage with OpenAI API. + /// public const string OpenAIBeta = "OpenAI-Beta"; + + /// + /// Header for specifying the OpenAI organization identifier. + /// public const string OpenAIOrganization = "OpenAI-Organization"; } /// - /// Audio format settings. + /// Audio format configurations supported by the Realtime API for input and output audio processing. /// public static class Audio { + /// + /// PCM 16-bit audio format identifier. + /// Used for both input_audio_format and output_audio_format in session configuration. + /// public const string FormatPcm16 = "pcm16"; + + /// + /// G.711 μ-law audio format identifier. + /// Used for both input_audio_format and output_audio_format in session configuration. + /// public const string FormatG711Ulaw = "g711_ulaw"; + + /// + /// G.711 A-law audio format identifier. + /// Used for both input_audio_format and output_audio_format in session configuration. + /// public const string FormatG711Alaw = "g711_alaw"; + + /// + /// Default sample rate for audio processing, specified in Hz. + /// public const int DefaultSampleRate = 24000; + + /// + /// Default number of bits per sample for audio processing. + /// public const int DefaultBitsPerSample = 16; + + /// + /// Default number of audio channels. + /// public const int DefaultChannels = 1; } /// - /// Turn detection settings. + /// Configuration settings for server-side Voice Activity Detection (VAD) turn detection. + /// These settings control how the server detects speech segments in audio input. /// public static class TurnDetection { + /// + /// Identifier for server-side Voice Activity Detection. + /// The only currently supported type for turn detection as specified in the API documentation. + /// public const string TypeServerVad = "server_vad"; + + /// + /// Default activation threshold for Voice Activity Detection (VAD). + /// Represents the sensitivity of speech detection, ranging from 0.0 to 1.0. + /// public const double DefaultThreshold = 0.5; + + /// + /// Default amount of audio to include before speech starts, in milliseconds. + /// This padding ensures the beginning of speech is not cut off. + /// public const int DefaultPrefixPaddingMs = 300; + + /// + /// Default duration of silence required to detect the end of speech, in milliseconds. + /// Used to determine when a speech segment has completed. + /// public const int DefaultSilenceDurationMs = 200; } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs index bb92ee94..9f74ae55 100644 --- a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEnums.cs @@ -3,6 +3,9 @@ namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; +/// +/// Converts between JSON strings and Status enum values for the OpenAI Realtime API. +/// internal class StatusJsonConverter : JsonConverter { public override Status Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -34,6 +37,9 @@ public override void Write(Utf8JsonWriter writer, Status value, JsonSerializerOp } } +/// +/// Converts between JSON strings and AudioFormat enum values for the OpenAI Realtime API. +/// internal class AudioFormatJsonConverter : JsonConverter { public override AudioFormat Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -61,6 +67,9 @@ public override void Write(Utf8JsonWriter writer, AudioFormat value, JsonSeriali } } +/// +/// Converts between JSON strings and ContentType enum values for the OpenAI Realtime API. +/// internal class ContentTypeJsonConverter : JsonConverter { public override ContentType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -90,6 +99,9 @@ public override void Write(Utf8JsonWriter writer, ContentType value, JsonSeriali } } +/// +/// Converts between JSON strings and ItemType enum values for the OpenAI Realtime API. +/// internal class ItemTypeJsonConverter : JsonConverter { public override ItemType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -117,6 +129,9 @@ public override void Write(Utf8JsonWriter writer, ItemType value, JsonSerializer } } +/// +/// Converts between JSON strings and Role enum values for the OpenAI Realtime API. +/// internal class RoleJsonConverter : JsonConverter { public override Role Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -144,54 +159,152 @@ public override void Write(Utf8JsonWriter writer, Role value, JsonSerializerOpti } } +/// +/// Represents the status of items in the OpenAI Realtime API responses. +/// [JsonConverter(typeof(StatusJsonConverter))] public enum Status { + /// + /// Represents an unimplemented or unknown status. + /// Unimplemented, + + /// + /// Indicates that the item has been completed successfully. + /// Completed, + + /// + /// Indicates that the item is currently in progress. + /// InProgress, + + /// + /// Indicates that the item is incomplete, which can occur when a response is interrupted. + /// Incomplete, + + /// + /// Indicates that the item was cancelled by the client. + /// Cancelled, + + /// + /// Indicates that the item failed to complete due to an error. + /// Failed } +/// +/// Represents the audio format options supported by the OpenAI Realtime API. +/// [JsonConverter(typeof(AudioFormatJsonConverter))] public enum AudioFormat { + /// + /// Represents an unimplemented or unknown audio format. + /// Unimplemented, + + /// + /// 16-bit PCM audio format. + /// PCM16, - // ReSharper disable once InconsistentNaming + /// + /// G.711 µ-law audio format. + /// G711_ULAW, - // ReSharper disable once InconsistentNaming + /// + /// G.711 A-law audio format. + /// G711_ALAW } +/// +/// Represents the content types supported in the OpenAI Realtime API messages. +/// [JsonConverter(typeof(ContentTypeJsonConverter))] public enum ContentType { + /// + /// Represents an unimplemented or unknown content type. + /// Unimplemented, + + /// + /// Represents input text content from the user. + /// InputText, + + /// + /// Represents input audio content from the user. + /// InputAudio, + + /// + /// Represents text content in the response. + /// Text, + + /// + /// Represents audio content in the response. + /// Audio } +/// +/// Represents the types of items that can appear in the OpenAI Realtime API conversation. +/// [JsonConverter(typeof(ItemTypeJsonConverter))] public enum ItemType { + /// + /// Represents an unimplemented or unknown item type. + /// Unimplemented, + + /// + /// Represents a message item containing text or audio content. + /// Message, + + /// + /// Represents a function call item made by the assistant. + /// FunctionCall, + + /// + /// Represents the output of a function call. + /// FunctionCallOutput } +/// +/// Represents the possible roles in the OpenAI Realtime API conversation. +/// [JsonConverter(typeof(RoleJsonConverter))] public enum Role { + /// + /// Represents an unimplemented or unknown role. + /// Unimplemented, + + /// + /// Represents the user role in the conversation. + /// User, + + /// + /// Represents the assistant role in the conversation. + /// Assistant, + + /// + /// Represents the system role in the conversation. + /// System } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs index 30b99179..15d29067 100644 --- a/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs +++ b/OpenAI.SDK/ObjectModels/RealtimeModels/RealtimeEventTypes.cs @@ -1,118 +1,311 @@ namespace Betalgo.Ranul.OpenAI.ObjectModels.RealtimeModels; +/// +/// Contains constant string values for all event types in the OpenAI Realtime WebSocket API. +/// public static class RealtimeEventTypes { + /// + /// Events that the OpenAI Realtime WebSocket server will accept from the client. + /// public static class Client { + /// + /// Session-related client events. + /// public static class Session { + /// + /// Event to update the session's default configuration. + /// public const string Update = "session.update"; } + /// + /// Input audio buffer-related client events. + /// public static class InputAudioBuffer { + /// + /// Event to append audio bytes to the input audio buffer. + /// public const string Append = "input_audio_buffer.append"; + + /// + /// Event to commit audio bytes to a user message. + /// public const string Commit = "input_audio_buffer.commit"; + + /// + /// Event to clear the audio bytes in the buffer. + /// public const string Clear = "input_audio_buffer.clear"; } + /// + /// Conversation-related client events. + /// public static class Conversation { + /// + /// Conversation item-related client events. + /// public static class Item { + /// + /// Event for adding an item to the conversation. + /// public const string Create = "conversation.item.create"; + + /// + /// Event when you want to truncate a previous assistant message's audio. + /// public const string Truncate = "conversation.item.truncate"; + + /// + /// Event when you want to remove any item from the conversation history. + /// public const string Delete = "conversation.item.delete"; } } + /// + /// Response-related client events. + /// public static class Response { + /// + /// Event to trigger a response generation. + /// public const string Create = "response.create"; + + /// + /// Event to cancel an in-progress response. + /// public const string Cancel = "response.cancel"; } } + /// + /// Events emitted from the OpenAI Realtime WebSocket server to the client. + /// public static class Server { + /// + /// Returned when an error occurs. + /// public const string Error = "error"; + /// + /// Session-related server events. + /// public static class Session { + /// + /// Returned when a session is created. Emitted automatically when a new connection is established. + /// public const string Created = "session.created"; + + /// + /// Returned when a session is updated. + /// public const string Updated = "session.updated"; } + /// + /// Conversation-related server events. + /// public static class Conversation { + /// + /// Returned when a conversation is created. Emitted right after session creation. + /// public const string Created = "conversation.created"; + /// + /// Conversation item-related server events. + /// public static class Item { + /// + /// Returned when a conversation item is created. + /// public const string Created = "conversation.item.created"; + + /// + /// Returned when an earlier assistant audio message item is truncated by the client. + /// public const string Truncated = "conversation.item.truncated"; + + /// + /// Returned when an item in the conversation is deleted. + /// public const string Deleted = "conversation.item.deleted"; + /// + /// Input audio transcription-related server events. + /// public static class InputAudioTranscription { + /// + /// Returned when input audio transcription is enabled and a transcription succeeds. + /// public const string Completed = "conversation.item.input_audio_transcription.completed"; + + /// + /// Returned when input audio transcription is configured, and a transcription request for a user message failed. + /// public const string Failed = "conversation.item.input_audio_transcription.failed"; } } } + /// + /// Input audio buffer-related server events. + /// public static class InputAudioBuffer { + /// + /// Returned when an input audio buffer is committed, either by the client or automatically in server VAD mode. + /// public const string Committed = "input_audio_buffer.committed"; + + /// + /// Returned when the input audio buffer is cleared by the client. + /// public const string Cleared = "input_audio_buffer.cleared"; + + /// + /// Returned in server turn detection mode when speech is detected. + /// public const string SpeechStarted = "input_audio_buffer.speech_started"; + + /// + /// Returned in server turn detection mode when speech stops. + /// public const string SpeechStopped = "input_audio_buffer.speech_stopped"; } + /// + /// Response-related server events. + /// public static class Response { + /// + /// Returned when a new Response is created. The first event of response creation, where the response is in an initial state of "in_progress". + /// public const string Created = "response.created"; + + /// + /// Returned when a Response is done streaming. Always emitted, no matter the final state. + /// public const string Done = "response.done"; + /// + /// Output item-related server events. + /// public static class OutputItem { + /// + /// Returned when a new Item is created during response generation. + /// public const string Added = "response.output_item.added"; + + /// + /// Returned when an Item is done streaming. Also emitted when a Response is interrupted, incomplete, or cancelled. + /// public const string Done = "response.output_item.done"; } + /// + /// Content part-related server events. + /// public static class ContentPart { + /// + /// Returned when a new content part is added to an assistant message item during response generation. + /// public const string Added = "response.content_part.added"; + + /// + /// Returned when a content part is done streaming in an assistant message item. Also emitted when a Response is interrupted, incomplete, or cancelled. + /// public const string Done = "response.content_part.done"; } + /// + /// Text-related server events. + /// public static class Text { + /// + /// Returned when the text value of a "text" content part is updated. + /// public const string Delta = "response.text.delta"; + + /// + /// Returned when the text value of a "text" content part is done streaming. Also emitted when a Response is interrupted, incomplete, or cancelled. + /// public const string Done = "response.text.done"; } + /// + /// Audio transcript-related server events. + /// public static class AudioTranscript { + /// + /// Returned when the model-generated transcription of audio output is updated. + /// public const string Delta = "response.audio_transcript.delta"; + + /// + /// Returned when the model-generated transcription of audio output is done streaming. Also emitted when a Response is interrupted, incomplete, or cancelled. + /// public const string Done = "response.audio_transcript.done"; } + /// + /// Audio-related server events. + /// public static class Audio { + /// + /// Returned when the model-generated audio is updated. + /// public const string Delta = "response.audio.delta"; + + /// + /// Returned when the model-generated audio is done. Also emitted when a Response is interrupted, incomplete, or cancelled. + /// public const string Done = "response.audio.done"; } + /// + /// Function call arguments-related server events. + /// public static class FunctionCallArguments { + /// + /// Returned when the model-generated function call arguments are updated. + /// public const string Delta = "response.function_call_arguments.delta"; + + /// + /// Returned when the model-generated function call arguments are done streaming. Also emitted when a Response is interrupted, incomplete, or cancelled. + /// public const string Done = "response.function_call_arguments.done"; } } + /// + /// Rate limits-related server events. + /// public static class RateLimits { + /// + /// Emitted after every "response.done" event to indicate the updated rate limits. + /// public const string Updated = "rate_limits.updated"; } } From e6617fcca01d3f8d0d6a40de018b050990ad0125 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Mon, 11 Nov 2024 11:40:04 +0000 Subject: [PATCH 07/12] RealtimeAudioExample updated --- .../TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs b/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs index b6a21e5e..50649bca 100644 --- a/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs +++ b/OpenAI.Playground/TestHelpers/RealtimeHelpers/RealtimeAudioExample.cs @@ -294,6 +294,12 @@ private void SetupEventHandlers() _ai.ServerEvents.OnError += (sender, args) => { Console.WriteLine($"Error: {args.Error.Message}"); }; + + // Debug event handler for all server events + //_ai.ServerEvents.OnAll += (sender, args) => + //{ + // Console.WriteLine($"Debug: {args}"); + //}; } /// From f2f07c301647afd7bb6f91683cb7df583c0d8ab4 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Mon, 11 Nov 2024 11:51:39 +0000 Subject: [PATCH 08/12] Version Bump --- OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj | 6 +++--- Readme.md | 25 +++++-------------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj b/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj index a4b9124e..d4c37800 100644 --- a/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj +++ b/OpenAI.SDK/Betalgo.Ranul.OpenAI.csproj @@ -10,13 +10,13 @@ Betalgo-Ranul-OpenAI-icon.png true OpenAI SDK by Betalgo - 8.8.0 + 8.9.0 Tolga Kayhan, Betalgo Betalgo Up Ltd. OpenAI .NET library by Betalgo Ranul - .NET library for the OpenAI service API by Betalgo Ranul + .NET library for the OpenAI services by Betalgo Ranul https://github.com/betalgo/openai/ - openAI,chatGPT,gpt-4, gpt-3,DALL·E,whisper,azureOpenAI,ai,betalgo,NLP,dalle,,dall-e,OpenAI,OpenAi,openAi,azure + openAI,realtime,chatGPT,gpt-4, gpt-3,DALL·E,whisper,azureOpenAI,ai,betalgo,NLP,dalle,dall-e,OpenAI,OpenAi,openAi,azure Betalgo.Ranul.OpenAI Readme.md True diff --git a/Readme.md b/Readme.md index be425635..7767efa6 100644 --- a/Readme.md +++ b/Readme.md @@ -26,6 +26,7 @@ Install-Package Betalgo.OpenAI.Utilities ``` ## Documentation and Links +- [Realtime API](https://github.com/betalgo/openai/wiki/realtime) ✨NEW - [Wiki Page](https://github.com/betalgo/openai/wiki) - [Feature Availability Table](https://github.com/betalgo/openai/wiki/Feature-Availability) - [Change Logs](https://github.com/betalgo/openai/wiki/Change-Logs) @@ -116,6 +117,10 @@ Due to time constraints, not all methods have been thoroughly tested or fully do Needless to say, I cannot accept responsibility for any damage caused by using the library. ## Changelog +### 8.9.0 +- Realtime API implementation is completed. As usual this is the first version and it may contain bugs. Please report any issues you encounter. +- [Realtime Sample](https://github.com/betalgo/openai/wiki/realtime) + ### 8.8.0 - **Compatibility Enhancement**: You can now use this library alongside the official OpenAI library and/or Semantic Kernel within the same project. The name changes in this update support this feature. @@ -135,26 +140,6 @@ Needless to say, I cannot accept responsibility for any damage caused by using t - **Utilities Library Status**: Please note that the Utilities library might remain broken for a while. I will focus on fixing it after completing the real-time API implementation. - -### 8.7.2 -- Fixed incorrect Azure Urls. -- Token usage response extended with `PromptTokensDetails`, `audio_tokens` and `cached_tokens`. -- Model list extended with `Gpt_4o_2024_08_06` and `Chatgpt_4o_latest`. - -### 8.7.1 -- moved `strict` paremeter from `ToolDefinition` to `FunctionDefinition` - -### 8.7.0 -- Added Support for o1 reasing models (`o1-mini` and `o1-preview`). -- Added `MaxCompletionTokens` for `chat completions`. -- Added support for `ParallelToolCalls` for `chat completions`. -- Added support for `ServiceTier` for `chat completions`. -- Added support for `ChunkingStrategy` in `Vector Store` and `Vector Store Files`. -- Added support for `Strict` in `ToolDefinition`. -- Added support for `MaxNumberResults` and `RankingOptions` for `FileSearchTool`. -- Added support for `ReasoningTokens` for `token usage`. -- Added support for `ResponseFormatOneOfType` for `AssistantResponse.cs`. - ### [More Change Logs](https://github.com/betalgo/openai/wiki/Change-Logs) --- From a03187e3823622b9fdad7ff4040f4bd74739642a Mon Sep 17 00:00:00 2001 From: jmuller3 Date: Mon, 28 Oct 2024 10:41:37 -0400 Subject: [PATCH 09/12] adds missing props to AssistantResponse from #657 --- .../SharedModels/AssistantResponse.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs index 82d2f3eb..fb0b8c9d 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs @@ -88,4 +88,20 @@ public record AssistantResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels /// [JsonPropertyName("tools")] public List Tools { get; set; } + + /// + /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while + /// lower values like 0.2 will make it more focused and deterministic. + /// + [JsonPropertyName("temperature")] + public float? Temperature { get; set; } + + /// + /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the + /// tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are + /// considered. + /// We generally recommend altering this or temperature but not both. + /// + [JsonPropertyName("top_p")] + public double? TopP { get; set; } } \ No newline at end of file From 4f00f2b943157ade8eb33aa1d9164619ce660161 Mon Sep 17 00:00:00 2001 From: Aleksandr Tarasov Date: Thu, 23 May 2024 09:12:58 +0500 Subject: [PATCH 10/12] fixed createdAt type --- .../BatchResponseModel/BatchResponse.cs | 2 +- .../ChatCompletionCreateResponse.cs | 6 +----- .../ResponseModels/CompletionCreateResponse.cs | 3 +-- .../ResponseModels/EditCreateResponse.cs | 3 +-- .../FileResponseModels/FileUploadResponse.cs | 3 +-- .../FineTuneResponseModels/FineTuneResponse.cs | 4 ++-- .../FineTuningJobResponse.cs | 2 +- .../ImageResponseModel/ImageCreateResponse.cs | 3 +-- .../ResponseModels/RunStepListResponse.cs | 2 +- .../VectorStoreFileBatchObject.cs | 2 +- .../VectorStoreFileObject.cs | 2 +- .../VectorStoreObjectResponse.cs | 2 +- .../SharedModels/AssistantFileResponse.cs | 2 +- .../SharedModels/AssistantResponse.cs | 2 +- .../ObjectModels/SharedModels/FileResponse.cs | 16 ++++------------ .../ObjectModels/SharedModels/IOpenAiModels.cs | 2 +- .../ObjectModels/SharedModels/MessageResponse.cs | 2 +- .../ObjectModels/SharedModels/RunResponse.cs | 2 +- .../ObjectModels/SharedModels/ThreadResponse.cs | 2 +- 19 files changed, 23 insertions(+), 39 deletions(-) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs index 8d2512e3..d4ad26a2 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs @@ -53,7 +53,7 @@ public record BatchResponse : BaseResponse, IOpenAIModels.IMetaData /// The Unix timestamp (in seconds) for when the batch was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The Unix timestamp (in seconds) for when the batch started processing. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs index c4207770..ec670c97 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs @@ -38,11 +38,7 @@ public record ChatCompletionCreateResponse : BaseResponse, IOpenAIModels.IId, IO [JsonPropertyName("service_tier")] public string? ServiceTier { get; set; } - /// - /// The Unix timestamp (in seconds) of when the chat completion was created. - /// - [JsonPropertyName("created")] - public int CreatedAt { get; set; } + [JsonPropertyName("created")] public long CreatedAt { get; set; } /// /// A unique identifier for the chat completion. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs index c4165a1a..fce10672 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs @@ -14,8 +14,7 @@ public record CompletionCreateResponse : BaseResponse, IOpenAIModels.IId, IOpenA [JsonPropertyName("usage")] public UsageResponse Usage { get; set; } - [JsonPropertyName("created")] - public int CreatedAt { get; set; } + [JsonPropertyName("created")] public long CreatedAt { get; set; } [JsonPropertyName("id")] public string Id { get; set; } diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs index 8e933fca..87e7afd2 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs @@ -14,6 +14,5 @@ public record EditCreateResponse : BaseResponse, IOpenAIModels.ICreatedAt [JsonPropertyName("usage")] public UsageResponse Usage { get; set; } - [JsonPropertyName("created")] - public int CreatedAt { get; set; } + [JsonPropertyName("created")] public long CreatedAt { get; set; } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs index 652c0615..4f4ce65a 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs @@ -17,6 +17,5 @@ public record FileUploadResponse : BaseResponse, IOpenAIModels.ICreatedAt [JsonPropertyName("purpose")] public string Purpose { get; set; } - [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + [JsonPropertyName("created_at")] public long CreatedAt { get; set; } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs index f75e5305..1e85699d 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs @@ -35,8 +35,8 @@ public record FineTuneResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels. [JsonPropertyName("created_at")] public int CreatedAt { get; set; } - [JsonPropertyName("id")] - public string Id { get; set; } + [JsonPropertyName("created_at")] public long CreatedAt { get; set; } + [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("model")] public string Model { get; set; } diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs index c7aa4c78..2172e7a2 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs @@ -67,7 +67,7 @@ public record FineTuningJobResponse : BaseResponse, IOpenAIModels.IId, IOpenAIMo /// The Unix timestamp (in seconds) for when the fine-tuning job was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The object identifier, which can be referenced in the API endpoints. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs index db02d838..de888183 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs @@ -8,8 +8,7 @@ public record ImageCreateResponse : BaseResponse, IOpenAIModels.ICreatedAt [JsonPropertyName("data")] public List Results { get; set; } - [JsonPropertyName("created")] - public int CreatedAt { get; set; } + [JsonPropertyName("created")] public long CreatedAt { get; set; } public record ImageDataResult { diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs index 41be6e64..361e6597 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs @@ -93,7 +93,7 @@ public record RunStepResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.I /// The Unix timestamp (in seconds) for when the run step was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The identifier of the run step, which can be referenced in API endpoints. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs index 60229ad4..fd5c205d 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs @@ -15,7 +15,7 @@ public record VectorStoreFileBatchObject : BaseResponse /// The Unix timestamp (in seconds) for when the vector store files batch was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs index d26dcb60..39742d59 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs @@ -26,7 +26,7 @@ public record VectorStoreFileObject : BaseResponse /// The Unix timestamp (in seconds) for when the vector store file was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs index b56b9ed6..ca5eed11 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs @@ -15,7 +15,7 @@ public record VectorStoreObjectResponse : BaseResponse /// The Unix timestamp (in seconds) for when the vector store was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The name of the vector store. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs index 98ad7930..e081d1d4 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs @@ -15,7 +15,7 @@ public record AssistantFileResponse : BaseResponse, IOpenAIModels.IId, IOpenAIMo /// The Unix timestamp (in seconds) for when the assistant file was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The identifier, which can be referenced in API endpoints. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs index fb0b8c9d..10fb29c0 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs @@ -62,7 +62,7 @@ public record AssistantResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels /// The Unix timestamp (in seconds) for when the assistant was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The identifier, which can be referenced in API endpoints. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs index 3c8f9ccb..2f6eceac 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs @@ -12,16 +12,8 @@ public record FileResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.ICre public string FileName { get; set; } public UploadFilePurposes.UploadFilePurpose PurposeEnum => UploadFilePurposes.ToEnum(Purpose); - - [JsonPropertyName("purpose")] - public string Purpose { get; set; } - - [JsonPropertyName("status")] - public string Status { get; set; } - - [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } - - [JsonPropertyName("id")] - public string Id { get; set; } + [JsonPropertyName("purpose")] public string Purpose { get; set; } + [JsonPropertyName("status")] public string Status { get; set; } + [JsonPropertyName("created_at")] public long CreatedAt { get; set; } + [JsonPropertyName("id")] public string Id { get; set; } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs b/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs index ad402598..778d88cf 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs @@ -36,7 +36,7 @@ public interface IAssistantId public interface ICreatedAt { - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } } public interface ICompletedAt diff --git a/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs index 1a6ac767..8295cea4 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs @@ -82,7 +82,7 @@ public MessageResponse Delta /// The Unix timestamp (in seconds) for when the message was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The identifier, which can be referenced in API endpoints. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs index 93205b0f..07bbd276 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs @@ -143,7 +143,7 @@ public record RunResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.IMode /// The Unix timestamp (in seconds) for when the run was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The list of File IDs the assistant used for this run. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs index cb97217d..bb6b2b9f 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs @@ -18,7 +18,7 @@ public record ThreadResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.IC /// The Unix timestamp (in seconds) for when the assistant was created. /// [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAt { get; set; } /// /// The identifier, which can be referenced in API endpoints. From 12a51c75813edb932ff4c4ed8e7d5de64db9912f Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Wed, 13 Nov 2024 13:16:24 +0000 Subject: [PATCH 11/12] Added support for DateTimeOffest for CreatedAt --- .../BatchResponseModel/BatchResponse.cs | 7 +++- .../ChatCompletionCreateResponse.cs | 5 ++- .../CompletionCreateResponse.cs | 5 ++- .../ResponseModels/EditCreateResponse.cs | 5 ++- .../FileResponseModels/FileUploadResponse.cs | 5 ++- .../FineTuneResponse.cs | 8 +++-- .../FineTuningJobResponse.cs | 7 +++- .../ImageResponseModel/ImageCreateResponse.cs | 4 ++- .../ResponseModels/RunStepListResponse.cs | 4 ++- .../VectorStoreFileBatchObject.cs | 6 +++- .../VectorStoreFileObject.cs | 7 +++- .../VectorStoreObjectResponse.cs | 7 +++- .../SharedModels/AssistantFileResponse.cs | 4 ++- .../SharedModels/AssistantResponse.cs | 36 ++++++++++--------- .../SharedModels/EventResponse.cs | 6 ++-- .../ObjectModels/SharedModels/FileResponse.cs | 18 +++++++--- .../SharedModels/IOpenAiModels.cs | 3 +- .../SharedModels/MessageResponse.cs | 7 +++- .../ObjectModels/SharedModels/RunResponse.cs | 7 +++- .../SharedModels/ThreadResponse.cs | 7 +++- 20 files changed, 116 insertions(+), 42 deletions(-) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs index d4ad26a2..714fb9ff 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/BatchResponseModel/BatchResponse.cs @@ -53,7 +53,12 @@ public record BatchResponse : BaseResponse, IOpenAIModels.IMetaData /// The Unix timestamp (in seconds) for when the batch was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + /// + /// DateTimeOffset for when the batch was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The Unix timestamp (in seconds) for when the batch started processing. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs index ec670c97..8bd01d3a 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/ChatCompletionCreateResponse.cs @@ -38,7 +38,10 @@ public record ChatCompletionCreateResponse : BaseResponse, IOpenAIModels.IId, IO [JsonPropertyName("service_tier")] public string? ServiceTier { get; set; } - [JsonPropertyName("created")] public long CreatedAt { get; set; } + [JsonPropertyName("created")] + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// A unique identifier for the chat completion. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs index fce10672..05a94c83 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/CompletionCreateResponse.cs @@ -14,7 +14,10 @@ public record CompletionCreateResponse : BaseResponse, IOpenAIModels.IId, IOpenA [JsonPropertyName("usage")] public UsageResponse Usage { get; set; } - [JsonPropertyName("created")] public long CreatedAt { get; set; } + [JsonPropertyName("created")] + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); [JsonPropertyName("id")] public string Id { get; set; } diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs index 87e7afd2..2064d42a 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/EditCreateResponse.cs @@ -14,5 +14,8 @@ public record EditCreateResponse : BaseResponse, IOpenAIModels.ICreatedAt [JsonPropertyName("usage")] public UsageResponse Usage { get; set; } - [JsonPropertyName("created")] public long CreatedAt { get; set; } + [JsonPropertyName("created")] + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs index 4f4ce65a..585be3f3 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/FileResponseModels/FileUploadResponse.cs @@ -17,5 +17,8 @@ public record FileUploadResponse : BaseResponse, IOpenAIModels.ICreatedAt [JsonPropertyName("purpose")] public string Purpose { get; set; } - [JsonPropertyName("created_at")] public long CreatedAt { get; set; } + [JsonPropertyName("created_at")] + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs index 1e85699d..8f3eca59 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuneResponseModels/FineTuneResponse.cs @@ -33,10 +33,12 @@ public record FineTuneResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels. public int? UpdatedAt { get; set; } [JsonPropertyName("created_at")] - public int CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } - [JsonPropertyName("created_at")] public long CreatedAt { get; set; } - [JsonPropertyName("id")] public string Id { get; set; } + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); + + [JsonPropertyName("id")] + public string Id { get; set; } [JsonPropertyName("model")] public string Model { get; set; } diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs index 2172e7a2..b3f940a8 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/FineTuningJobResponseModels/FineTuningJobResponse.cs @@ -67,7 +67,12 @@ public record FineTuningJobResponse : BaseResponse, IOpenAIModels.IId, IOpenAIMo /// The Unix timestamp (in seconds) for when the fine-tuning job was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + /// + /// for when the fine-tuning job was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The object identifier, which can be referenced in the API endpoints. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs index de888183..4542c5b3 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/ImageResponseModel/ImageCreateResponse.cs @@ -8,7 +8,9 @@ public record ImageCreateResponse : BaseResponse, IOpenAIModels.ICreatedAt [JsonPropertyName("data")] public List Results { get; set; } - [JsonPropertyName("created")] public long CreatedAt { get; set; } + [JsonPropertyName("created")] + public long CreatedAtUnix { get; set; } + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); public record ImageDataResult { diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs index 361e6597..5b9946a4 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/RunStepListResponse.cs @@ -93,7 +93,9 @@ public record RunStepResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.I /// The Unix timestamp (in seconds) for when the run step was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The identifier of the run step, which can be referenced in API endpoints. diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs index fd5c205d..e3643682 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileBatchObject.cs @@ -15,7 +15,11 @@ public record VectorStoreFileBatchObject : BaseResponse /// The Unix timestamp (in seconds) for when the vector store files batch was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + /// + /// for when the vector store files batch was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs index 39742d59..e0d77be1 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreFileObject.cs @@ -26,7 +26,12 @@ public record VectorStoreFileObject : BaseResponse /// The Unix timestamp (in seconds) for when the vector store file was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + /// + /// for when the vector store file was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The ID of the [vector store](/docs/api-reference/vector-stores/object) that the [File](/docs/api-reference/files) diff --git a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs index ca5eed11..46e79bc3 100644 --- a/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs +++ b/OpenAI.SDK/ObjectModels/ResponseModels/VectorStoreResponseModels/VectorStoreObjectResponse.cs @@ -15,7 +15,12 @@ public record VectorStoreObjectResponse : BaseResponse /// The Unix timestamp (in seconds) for when the vector store was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + /// + /// for when the vector store was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The name of the vector store. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs index e081d1d4..9fe7166c 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/AssistantFileResponse.cs @@ -15,7 +15,9 @@ public record AssistantFileResponse : BaseResponse, IOpenAIModels.IId, IOpenAIMo /// The Unix timestamp (in seconds) for when the assistant file was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The identifier, which can be referenced in API endpoints. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs index 10fb29c0..268e4947 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/AssistantResponse.cs @@ -58,11 +58,29 @@ public record AssistantResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels [JsonPropertyName("response_format")] public ResponseFormatOneOfType ResponseFormatOneOfType { get; set; } + /// + /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while + /// lower values like 0.2 will make it more focused and deterministic. + /// + [JsonPropertyName("temperature")] + public float? Temperature { get; set; } + + /// + /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the + /// tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are + /// considered. + /// We generally recommend altering this or temperature but not both. + /// + [JsonPropertyName("top_p")] + public double? TopP { get; set; } + /// /// The Unix timestamp (in seconds) for when the assistant was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The identifier, which can be referenced in API endpoints. @@ -88,20 +106,4 @@ public record AssistantResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels /// [JsonPropertyName("tools")] public List Tools { get; set; } - - /// - /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while - /// lower values like 0.2 will make it more focused and deterministic. - /// - [JsonPropertyName("temperature")] - public float? Temperature { get; set; } - - /// - /// An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the - /// tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are - /// considered. - /// We generally recommend altering this or temperature but not both. - /// - [JsonPropertyName("top_p")] - public double? TopP { get; set; } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/SharedModels/EventResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/EventResponse.cs index ae0d6766..35b6f06f 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/EventResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/EventResponse.cs @@ -2,7 +2,7 @@ namespace Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; -public record EventResponse +public record EventResponse:IOpenAIModels.ICreatedAt { [JsonPropertyName("object")] public string? ObjectTypeName { get; set; } @@ -11,7 +11,9 @@ public record EventResponse public string? Id { get; set; } [JsonPropertyName("created_at")] - public int? CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); + [JsonPropertyName("level")] public string Level { get; set; } diff --git a/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs index 2f6eceac..97f90049 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/FileResponse.cs @@ -12,8 +12,18 @@ public record FileResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.ICre public string FileName { get; set; } public UploadFilePurposes.UploadFilePurpose PurposeEnum => UploadFilePurposes.ToEnum(Purpose); - [JsonPropertyName("purpose")] public string Purpose { get; set; } - [JsonPropertyName("status")] public string Status { get; set; } - [JsonPropertyName("created_at")] public long CreatedAt { get; set; } - [JsonPropertyName("id")] public string Id { get; set; } + + [JsonPropertyName("purpose")] + public string Purpose { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("created_at")] + public long CreatedAtUnix { get; set; } + + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); + + [JsonPropertyName("id")] + public string Id { get; set; } } \ No newline at end of file diff --git a/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs b/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs index 778d88cf..10aaba7c 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/IOpenAiModels.cs @@ -36,7 +36,8 @@ public interface IAssistantId public interface ICreatedAt { - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + public DateTimeOffset CreatedAt { get; } } public interface ICompletedAt diff --git a/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs index 8295cea4..1d2c5cc5 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/MessageResponse.cs @@ -14,6 +14,7 @@ public MessageResponse Delta { set => Content = value.Content; } + /// /// The thread ID that this message belongs to. /// @@ -82,7 +83,11 @@ public MessageResponse Delta /// The Unix timestamp (in seconds) for when the message was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + /// + /// for when the message was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The identifier, which can be referenced in API endpoints. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs index 07bbd276..19568ed5 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/RunResponse.cs @@ -143,7 +143,12 @@ public record RunResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.IMode /// The Unix timestamp (in seconds) for when the run was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + /// + /// for when the run was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The list of File IDs the assistant used for this run. diff --git a/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs b/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs index bb6b2b9f..c8c7e209 100644 --- a/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs +++ b/OpenAI.SDK/ObjectModels/SharedModels/ThreadResponse.cs @@ -18,7 +18,12 @@ public record ThreadResponse : BaseResponse, IOpenAIModels.IId, IOpenAIModels.IC /// The Unix timestamp (in seconds) for when the assistant was created. /// [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } + public long CreatedAtUnix { get; set; } + + /// + /// for when the assistant was created. + /// + public DateTimeOffset CreatedAt => DateTimeOffset.FromUnixTimeSeconds(CreatedAtUnix); /// /// The identifier, which can be referenced in API endpoints. From cafa2ad16f580ad80552151bab3423222c628632 Mon Sep 17 00:00:00 2001 From: Tolga Kayhan Date: Wed, 13 Nov 2024 13:52:14 +0000 Subject: [PATCH 12/12] Refactor methods and improve code readability Reorganized `using` directives and added new namespaces. Modified `ChatClientMetadata` for better readability. Expanded `GetService` and `Dispose` methods to use block bodies. Refactored `CompleteAsync`, `CompleteStreamingAsync`, `CreateRequest`, and other methods to use `var` and simplify loops and statements. Improved error messages and renamed variables for clarity. --- OpenAI.SDK/Managers/OpenAIChatClient.cs | 167 ++++++++++++------------ 1 file changed, 85 insertions(+), 82 deletions(-) diff --git a/OpenAI.SDK/Managers/OpenAIChatClient.cs b/OpenAI.SDK/Managers/OpenAIChatClient.cs index 4e7049a6..57ba3117 100644 --- a/OpenAI.SDK/Managers/OpenAIChatClient.cs +++ b/OpenAI.SDK/Managers/OpenAIChatClient.cs @@ -1,9 +1,11 @@ -using Betalgo.Ranul.OpenAI.ObjectModels; +using System.Runtime.CompilerServices; +using System.Text.Json; +using Betalgo.Ranul.OpenAI.ObjectModels; using Betalgo.Ranul.OpenAI.ObjectModels.RequestModels; using Betalgo.Ranul.OpenAI.ObjectModels.ResponseModels; using Betalgo.Ranul.OpenAI.ObjectModels.SharedModels; using Microsoft.Extensions.AI; -using System.Text.Json; +using ChatMessage = Microsoft.Extensions.AI.ChatMessage; namespace Betalgo.Ranul.OpenAI.Managers; @@ -11,32 +13,35 @@ public partial class OpenAIService : IChatClient { private ChatClientMetadata? _chatMetadata; - /// - ChatClientMetadata IChatClient.Metadata => _chatMetadata ??= new(nameof(OpenAIService), _httpClient.BaseAddress, this._defaultModelId); + /// + ChatClientMetadata IChatClient.Metadata => _chatMetadata ??= new(nameof(OpenAIService), _httpClient.BaseAddress, _defaultModelId); - /// - TService? IChatClient.GetService(object? key) where TService : class => - this as TService; + /// + TService? IChatClient.GetService(object? key) where TService : class + { + return this as TService; + } - /// - void IDisposable.Dispose() { } + /// + void IDisposable.Dispose() + { + } - /// - async Task IChatClient.CompleteAsync( - IList chatMessages, ChatOptions? options, CancellationToken cancellationToken) + /// + async Task IChatClient.CompleteAsync(IList chatMessages, ChatOptions? options, CancellationToken cancellationToken) { - ChatCompletionCreateRequest request = CreateRequest(chatMessages, options); + var request = CreateRequest(chatMessages, options); - var response = await this.ChatCompletion.CreateCompletion(request, options?.ModelId, cancellationToken); + var response = await ChatCompletion.CreateCompletion(request, options?.ModelId, cancellationToken); ThrowIfNotSuccessful(response); string? finishReason = null; - List responseMessages = []; - foreach (ChatChoiceResponse choice in response.Choices) + List responseMessages = []; + foreach (var choice in response.Choices) { finishReason ??= choice.FinishReason; - Microsoft.Extensions.AI.ChatMessage m = new() + ChatMessage m = new() { Role = new(choice.Message.Role), AuthorName = choice.Message.Name, @@ -60,36 +65,35 @@ async Task IChatClient.CompleteAsync( return new(responseMessages) { - CreatedAt = DateTimeOffset.FromUnixTimeSeconds(response.CreatedAt), + CreatedAt = response.CreatedAt, CompletionId = response.Id, FinishReason = finishReason is not null ? new(finishReason) : null, ModelId = response.Model, RawRepresentation = response, - Usage = response.Usage is { } usage ? GetUsageDetails(usage) : null, + Usage = response.Usage is { } usage ? GetUsageDetails(usage) : null }; } - /// - async IAsyncEnumerable IChatClient.CompleteStreamingAsync( - IList chatMessages, ChatOptions? options, CancellationToken cancellationToken) + /// + async IAsyncEnumerable IChatClient.CompleteStreamingAsync(IList chatMessages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) { - ChatCompletionCreateRequest request = CreateRequest(chatMessages, options); + var request = CreateRequest(chatMessages, options); - await foreach (var response in this.ChatCompletion.CreateCompletionAsStream(request, options?.ModelId, cancellationToken: cancellationToken)) + await foreach (var response in ChatCompletion.CreateCompletionAsStream(request, options?.ModelId, cancellationToken: cancellationToken)) { ThrowIfNotSuccessful(response); - foreach (ChatChoiceResponse choice in response.Choices) + foreach (var choice in response.Choices) { StreamingChatCompletionUpdate update = new() { AuthorName = choice.Delta.Name, CompletionId = response.Id, - CreatedAt = DateTimeOffset.FromUnixTimeSeconds(response.CreatedAt), + CreatedAt = response.CreatedAt, FinishReason = choice.FinishReason is not null ? new(choice.FinishReason) : null, ModelId = response.Model, RawRepresentation = response, - Role = choice.Delta.Role is not null ? new(choice.Delta.Role) : null, + Role = choice.Delta.Role is not null ? new(choice.Delta.Role) : null }; if (choice.Index is not null) @@ -118,10 +122,10 @@ async IAsyncEnumerable IChatClient.CompleteStream AuthorName = choice.Delta.Name, CompletionId = response.Id, Contents = [new UsageContent(GetUsageDetails(usage))], - CreatedAt = DateTimeOffset.FromUnixTimeSeconds(response.CreatedAt), + CreatedAt = response.CreatedAt, FinishReason = choice.FinishReason is not null ? new(choice.FinishReason) : null, ModelId = response.Model, - Role = choice.Delta.Role is not null ? new(choice.Delta.Role) : null, + Role = choice.Delta.Role is not null ? new(choice.Delta.Role) : null }; } } @@ -132,14 +136,11 @@ private static void ThrowIfNotSuccessful(ChatCompletionCreateResponse response) { if (!response.Successful) { - throw new InvalidOperationException(response.Error is { } error ? - $"{response.Error.Code}: {response.Error.Message}" : - "Unknown error"); + throw new InvalidOperationException(response.Error is { } error ? $"{response.Error.Code}: {response.Error.Message}" : "Betalgo.Ranul Unknown error"); } } - private ChatCompletionCreateRequest CreateRequest( - IList chatMessages, ChatOptions? options) + private ChatCompletionCreateRequest CreateRequest(IList chatMessages, ChatOptions? options) { ChatCompletionCreateRequest request = new() { @@ -157,7 +158,7 @@ private ChatCompletionCreateRequest CreateRequest( request.StopAsList = options.StopSequences; // Non-strongly-typed properties from additional properties - request.LogitBias = options.AdditionalProperties?.TryGetValue(nameof(request.LogitBias), out object? logitBias) is true ? logitBias : null; + request.LogitBias = options.AdditionalProperties?.TryGetValue(nameof(request.LogitBias), out var logitBias) is true ? logitBias : null; request.LogProbs = options.AdditionalProperties?.TryGetValue(nameof(request.LogProbs), out bool logProbs) is true ? logProbs : null; request.N = options.AdditionalProperties?.TryGetValue(nameof(request.N), out int n) is true ? n : null; request.ParallelToolCalls = options.AdditionalProperties?.TryGetValue(nameof(request.ParallelToolCalls), out bool parallelToolCalls) is true ? parallelToolCalls : null; @@ -173,15 +174,15 @@ private ChatCompletionCreateRequest CreateRequest( request.ResponseFormat = new() { Type = StaticValues.CompletionStatics.ResponseFormat.Text }; break; - case ChatResponseFormatJson json when json.Schema is not null: + case ChatResponseFormatJson { Schema: not null } json: request.ResponseFormat = new() { Type = StaticValues.CompletionStatics.ResponseFormat.JsonSchema, - JsonSchema = new JsonSchema() + JsonSchema = new() { Name = json.SchemaName ?? "JsonSchema", Schema = JsonSerializer.Deserialize(json.Schema), - Description = json.SchemaDescription, + Description = json.SchemaDescription } }; break; @@ -192,25 +193,28 @@ private ChatCompletionCreateRequest CreateRequest( } // Tools - request.Tools = options.Tools?.OfType().Select(f => - { - return ToolDefinition.DefineFunction(new FunctionDefinition() + request.Tools = options.Tools + ?.OfType() + .Select(f => { - Name = f.Metadata.Name, - Description = f.Metadata.Description, - Parameters = CreateParameters(f) - }); - }).ToList() is { Count: > 0 } tools ? tools : null; + return ToolDefinition.DefineFunction(new() + { + Name = f.Metadata.Name, + Description = f.Metadata.Description, + Parameters = CreateParameters(f) + }); + }) + .ToList() is { Count: > 0 } tools + ? tools + : null; if (request.Tools is not null) { - request.ToolChoice = - options.ToolMode is RequiredChatToolMode r ? new ToolChoice() + request.ToolChoice = options.ToolMode is RequiredChatToolMode r ? new() { Type = StaticValues.CompletionStatics.ToolChoiceType.Required, Function = r.RequiredFunctionName is null ? null : new ToolChoice.FunctionTool() { Name = r.RequiredFunctionName } } : - options.ToolMode is AutoChatToolMode ? new ToolChoice() { Type = StaticValues.CompletionStatics.ToolChoiceType.Auto } : - new ToolChoice() { Type = StaticValues.CompletionStatics.ToolChoiceType.None }; + options.ToolMode is AutoChatToolMode ? new() { Type = StaticValues.CompletionStatics.ToolChoiceType.Auto } : new ToolChoice() { Type = StaticValues.CompletionStatics.ToolChoiceType.None }; } } @@ -227,24 +231,27 @@ private ChatCompletionCreateRequest CreateRequest( { Content = tc.Text, Name = message.AuthorName, - Role = message.Role.ToString(), + Role = message.Role.ToString() }); break; case ImageContent ic: request.Messages.Add(new() { - Contents = [new() - { - Type = "image_url", - ImageUrl = new MessageImageUrl() + Contents = + [ + new() { - Url = ic.Uri, - Detail = ic.AdditionalProperties?.TryGetValue(nameof(MessageImageUrl.Detail), out string? detail) is true ? detail : null, - }, - }], + Type = "image_url", + ImageUrl = new() + { + Url = ic.Uri, + Detail = ic.AdditionalProperties?.TryGetValue(nameof(MessageImageUrl.Detail), out string? detail) is true ? detail : null + } + } + ], Name = message.AuthorName, - Role = message.Role.ToString(), + Role = message.Role.ToString() }); break; @@ -254,29 +261,30 @@ private ChatCompletionCreateRequest CreateRequest( ToolCallId = frc.CallId, Content = frc.Result?.ToString(), Name = message.AuthorName, - Role = message.Role.ToString(), + Role = message.Role.ToString() }); break; } } - FunctionCallContent[] fccs = message.Contents.OfType().ToArray(); - if (fccs.Length > 0) + var functionCallContents = message.Contents.OfType().ToArray(); + if (functionCallContents.Length > 0) { request.Messages.Add(new() { Name = message.AuthorName, Role = message.Role.ToString(), - ToolCalls = fccs.Select(fcc => new ToolCall() - { - Type = "function", - Id = fcc.CallId, - FunctionCall = new FunctionCall() + ToolCalls = functionCallContents.Select(fcc => new ToolCall() { - Name = fcc.Name, - Arguments = JsonSerializer.Serialize(fcc.Arguments) - }, - }).ToList(), + Type = "function", + Id = fcc.CallId, + FunctionCall = new() + { + Name = fcc.Name, + Arguments = JsonSerializer.Serialize(fcc.Arguments) + } + }) + .ToList() }); } } @@ -291,11 +299,9 @@ private static PropertyDefinition CreateParameters(AIFunction f) var parameters = f.Metadata.Parameters; - foreach (AIFunctionParameterMetadata parameter in parameters) + foreach (var parameter in parameters) { - properties.Add(parameter.Name, parameter.Schema is JsonElement e ? - e.Deserialize()! : - PropertyDefinition.DefineObject(null, null, null, null, null)); + properties.Add(parameter.Name, parameter.Schema is JsonElement e ? e.Deserialize()! : PropertyDefinition.DefineObject(null, null, null, null, null)); if (parameter.IsRequired) { @@ -315,7 +321,7 @@ private static void PopulateContents(ObjectModels.RequestModels.ChatMessage sour if (source.Contents is { } contents) { - foreach (MessageContent content in contents) + foreach (var content in contents) { if (content.Text is string text) { @@ -333,10 +339,7 @@ private static void PopulateContents(ObjectModels.RequestModels.ChatMessage sour { foreach (var tc in toolCalls) { - destination.Add(new FunctionCallContent( - tc.Id ?? string.Empty, - tc.FunctionCall?.Name ?? string.Empty, - tc.FunctionCall?.Arguments is string a ? JsonSerializer.Deserialize>(a) : null)); + destination.Add(new FunctionCallContent(tc.Id ?? string.Empty, tc.FunctionCall?.Name ?? string.Empty, tc.FunctionCall?.Arguments is string a ? JsonSerializer.Deserialize>(a) : null)); } } } @@ -347,7 +350,7 @@ private static UsageDetails GetUsageDetails(UsageResponse usage) { InputTokenCount = usage.PromptTokens, OutputTokenCount = usage.CompletionTokens, - TotalTokenCount = usage.TotalTokens, + TotalTokenCount = usage.TotalTokens }; if (usage.PromptTokensDetails is { } promptDetails)