From 2e824216bd90948a0546038a1abc6d9e205dd42e Mon Sep 17 00:00:00 2001 From: HavenDV Date: Wed, 15 Nov 2023 05:23:32 +0400 Subject: [PATCH] feat: Added netstandard support. --- src/libs/OpenAI/Client/Audio/AudioEndpoint.cs | 12 +- src/libs/OpenAI/Client/Chat/ChatEndpoint.cs | 4 +- .../Client/Completions/CompletionsEndpoint.cs | 4 +- .../Extensions/JsonStringEnumConverter.cs | 2 +- .../Client/Extensions/ResponseExtensions.cs | 8 +- src/libs/OpenAI/Client/Files/FilesEndpoint.cs | 10 +- .../OpenAI/Client/Images/ImagesEndpoint.cs | 6 +- src/libs/OpenAI/Client/OpenAIClient.cs | 7 +- src/libs/OpenAI/OpenAI.csproj | 2 + src/libs/OpenAI/OpenAiApi.Constructors.cs | 24 --- src/libs/OpenAI/OpenAiApi.Embedding.cs | 35 ----- .../OpenAI/OpenAiApi.EnumsBugWorkaround.cs | 70 --------- src/libs/OpenAI/OpenAiApi.Streaming.cs | 138 ------------------ 13 files changed, 27 insertions(+), 295 deletions(-) delete mode 100755 src/libs/OpenAI/OpenAiApi.Constructors.cs delete mode 100755 src/libs/OpenAI/OpenAiApi.Embedding.cs delete mode 100755 src/libs/OpenAI/OpenAiApi.EnumsBugWorkaround.cs delete mode 100755 src/libs/OpenAI/OpenAiApi.Streaming.cs diff --git a/src/libs/OpenAI/Client/Audio/AudioEndpoint.cs b/src/libs/OpenAI/Client/Audio/AudioEndpoint.cs index bb4284a0..0b37f59c 100644 --- a/src/libs/OpenAI/Client/Audio/AudioEndpoint.cs +++ b/src/libs/OpenAI/Client/Audio/AudioEndpoint.cs @@ -44,15 +44,15 @@ public async Task> CreateSpeechAsync(SpeechRequest request, var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug); var response = await Api.Client.PostAsync(GetUrl("/speech"), jsonContent, cancellationToken).ConfigureAwait(false); await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var memoryStream = new MemoryStream(); + using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var memoryStream = new MemoryStream(); int bytesRead; var totalBytesRead = 0; var buffer = new byte[8192]; - while ((bytesRead = await responseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) + while ((bytesRead = await responseStream.ReadAsync(buffer, 0, 8192, cancellationToken).ConfigureAwait(false)) > 0) { - await memoryStream.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); + await memoryStream.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); if (chunkCallback != null) { @@ -82,7 +82,7 @@ public async Task CreateTranscriptionAsync(AudioTranscriptionRequest req { using var content = new MultipartFormDataContent(); using var audioData = new MemoryStream(); - await request.Audio.CopyToAsync(audioData, cancellationToken).ConfigureAwait(false); + await request.Audio.CopyToAsync(audioData).ConfigureAwait(false); content.Add(new ByteArrayContent(audioData.ToArray()), "file", request.AudioName); content.Add(new StringContent(request.Model), "model"); @@ -124,7 +124,7 @@ public async Task CreateTranslationAsync(AudioTranslationRequest request { using var content = new MultipartFormDataContent(); using var audioData = new MemoryStream(); - await request.Audio.CopyToAsync(audioData, cancellationToken).ConfigureAwait(false); + await request.Audio.CopyToAsync(audioData).ConfigureAwait(false); content.Add(new ByteArrayContent(audioData.ToArray()), "file", request.AudioName); content.Add(new StringContent(request.Model), "model"); diff --git a/src/libs/OpenAI/Client/Chat/ChatEndpoint.cs b/src/libs/OpenAI/Client/Chat/ChatEndpoint.cs index c41b7c01..35e672f8 100644 --- a/src/libs/OpenAI/Client/Chat/ChatEndpoint.cs +++ b/src/libs/OpenAI/Client/Chat/ChatEndpoint.cs @@ -52,7 +52,7 @@ public async Task StreamCompletionAsync(ChatRequest chatRequest, A request.Content = jsonContent; var response = await Api.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var reader = new StreamReader(stream); ChatResponse chatResponse = null; @@ -108,7 +108,7 @@ public async IAsyncEnumerable StreamCompletionEnumerableAsync(Chat request.Content = jsonContent; var response = await Api.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var reader = new StreamReader(stream); ChatResponse chatResponse = null; diff --git a/src/libs/OpenAI/Client/Completions/CompletionsEndpoint.cs b/src/libs/OpenAI/Client/Completions/CompletionsEndpoint.cs index 9499675b..92832d60 100644 --- a/src/libs/OpenAI/Client/Completions/CompletionsEndpoint.cs +++ b/src/libs/OpenAI/Client/Completions/CompletionsEndpoint.cs @@ -203,7 +203,7 @@ public async Task StreamCompletionAsync(CompletionRequest completionRequest, Act request.Content = jsonContent; var response = await Api.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var reader = new StreamReader(stream); while (await reader.ReadLineAsync() is { } streamData) @@ -311,7 +311,7 @@ public async IAsyncEnumerable StreamCompletionEnumerableAsync( request.Content = jsonContent; var response = await Api.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); using var reader = new StreamReader(stream); while (await reader.ReadLineAsync() is { } streamData) diff --git a/src/libs/OpenAI/Client/Extensions/JsonStringEnumConverter.cs b/src/libs/OpenAI/Client/Extensions/JsonStringEnumConverter.cs index 900950d7..ae030b7e 100644 --- a/src/libs/OpenAI/Client/Extensions/JsonStringEnumConverter.cs +++ b/src/libs/OpenAI/Client/Extensions/JsonStringEnumConverter.cs @@ -23,7 +23,7 @@ public JsonStringEnumConverter() namingPolicy = new SnakeCaseNamingPolicy(); var type = typeof(TEnum); - foreach (var value in Enum.GetValues()) + foreach (var value in Enum.GetValues(typeof(TEnum)).Cast()) { var enumMember = type.GetMember(value.ToString())[0]; var attribute = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false) diff --git a/src/libs/OpenAI/Client/Extensions/ResponseExtensions.cs b/src/libs/OpenAI/Client/Extensions/ResponseExtensions.cs index 8706c034..423f37de 100644 --- a/src/libs/OpenAI/Client/Extensions/ResponseExtensions.cs +++ b/src/libs/OpenAI/Client/Extensions/ResponseExtensions.cs @@ -92,11 +92,11 @@ internal static void SetResponseData(this BaseResponse response, HttpResponseHea internal static async Task ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse = false, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null) { - var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var responseAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) { - throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); + throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null); } if (debugResponse) @@ -111,8 +111,8 @@ internal static async Task CheckResponseAsync(this HttpResponseMessage response, { if (!response.IsSuccessStatusCode) { - var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode); + var responseAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null); } } diff --git a/src/libs/OpenAI/Client/Files/FilesEndpoint.cs b/src/libs/OpenAI/Client/Files/FilesEndpoint.cs index 6f957c93..9b72433a 100644 --- a/src/libs/OpenAI/Client/Files/FilesEndpoint.cs +++ b/src/libs/OpenAI/Client/Files/FilesEndpoint.cs @@ -79,7 +79,7 @@ public async Task UploadFileAsync(FileUploadRequest request, Cancellat { using var fileData = new MemoryStream(); using var content = new MultipartFormDataContent(); - await request.File.CopyToAsync(fileData, cancellationToken).ConfigureAwait(false); + await request.File.CopyToAsync(fileData).ConfigureAwait(false); content.Add(new StringContent(request.Purpose), "purpose"); content.Add(new ByteArrayContent(fileData.ToArray()), "file", request.FileName); request.Dispose(); @@ -104,7 +104,7 @@ async Task InternalDeleteFileAsync(int attempt) { var response = await Api.Client.DeleteAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false); // We specifically don't use the extension method here bc we need to check if it's still processing the file. - var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var responseAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) { @@ -186,9 +186,9 @@ public async Task DownloadFileAsync(FileData fileData, string directory, } } - await using var response = await Api.Client.GetStreamAsync(GetUrl($"/{fileData.Id}/content"), cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(filePath, FileMode.CreateNew); - await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); + using var response = await Api.Client.GetStreamAsync(GetUrl($"/{fileData.Id}/content")).ConfigureAwait(false); + using var fileStream = new FileStream(filePath, FileMode.CreateNew); + await response.CopyToAsync(fileStream).ConfigureAwait(false); return filePath; } } diff --git a/src/libs/OpenAI/Client/Images/ImagesEndpoint.cs b/src/libs/OpenAI/Client/Images/ImagesEndpoint.cs index fbd271f1..eb485a9d 100644 --- a/src/libs/OpenAI/Client/Images/ImagesEndpoint.cs +++ b/src/libs/OpenAI/Client/Images/ImagesEndpoint.cs @@ -123,13 +123,13 @@ public async Task> CreateImageEditAsync(ImageEditRequest r { using var content = new MultipartFormDataContent(); using var imageData = new MemoryStream(); - await request.Image.CopyToAsync(imageData, cancellationToken).ConfigureAwait(false); + await request.Image.CopyToAsync(imageData).ConfigureAwait(false); content.Add(new ByteArrayContent(imageData.ToArray()), "image", request.ImageName); if (request.Mask != null) { using var maskData = new MemoryStream(); - await request.Mask.CopyToAsync(maskData, cancellationToken).ConfigureAwait(false); + await request.Mask.CopyToAsync(maskData).ConfigureAwait(false); content.Add(new ByteArrayContent(maskData.ToArray()), "mask", request.MaskName); } @@ -192,7 +192,7 @@ public async Task> CreateImageVariationAsync(ImageVariatio { using var content = new MultipartFormDataContent(); using var imageData = new MemoryStream(); - await request.Image.CopyToAsync(imageData, cancellationToken).ConfigureAwait(false); + await request.Image.CopyToAsync(imageData).ConfigureAwait(false); content.Add(new ByteArrayContent(imageData.ToArray()), "image", request.ImageName); content.Add(new StringContent(request.Number.ToString()), "n"); content.Add(new StringContent(request.Size), "size"); diff --git a/src/libs/OpenAI/Client/OpenAIClient.cs b/src/libs/OpenAI/Client/OpenAIClient.cs index 501665f2..bd92364c 100644 --- a/src/libs/OpenAI/Client/OpenAIClient.cs +++ b/src/libs/OpenAI/Client/OpenAIClient.cs @@ -61,12 +61,9 @@ public OpenAIClient(OpenAIAuthentication openAIAuthentication = null, OpenAIClie ModerationsEndpoint = new ModerationsEndpoint(this); } - private HttpClient SetupClient(HttpClient client = null) + private HttpClient SetupClient(HttpClient? client = null) { - client ??= new HttpClient(new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15) - }); + client ??= new HttpClient(); client.DefaultRequestHeaders.Add("User-Agent", "OpenAI-DotNet"); if (!OpenAIClientSettings.BaseRequestUrlFormat.Contains(OpenAIClientSettings.AzureOpenAIDomain) && diff --git a/src/libs/OpenAI/OpenAI.csproj b/src/libs/OpenAI/OpenAI.csproj index 446626b9..f51e7313 100644 --- a/src/libs/OpenAI/OpenAI.csproj +++ b/src/libs/OpenAI/OpenAI.csproj @@ -32,11 +32,13 @@ + + diff --git a/src/libs/OpenAI/OpenAiApi.Constructors.cs b/src/libs/OpenAI/OpenAiApi.Constructors.cs deleted file mode 100755 index 317c6b7e..00000000 --- a/src/libs/OpenAI/OpenAiApi.Constructors.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Net.Http.Headers; - -namespace tryAGI.OpenAI; - -/// -/// Class providing methods for API access. -/// -public partial class OpenAiApi -{ - /// - /// Sets the selected apiKey as a default header for the HttpClient. - /// - /// - /// - public OpenAiApi(string apiKey, HttpClient httpClient) : this(httpClient) - { - apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); - httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( - scheme: "Bearer", - parameter: apiKey); - } -} diff --git a/src/libs/OpenAI/OpenAiApi.Embedding.cs b/src/libs/OpenAI/OpenAiApi.Embedding.cs deleted file mode 100755 index a531783f..00000000 --- a/src/libs/OpenAI/OpenAiApi.Embedding.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace tryAGI.OpenAI; - -public partial class OpenAiApi -{ - /// - /// User-friendly method for creating an embedding. - /// Defaults to the model. - /// - /// - /// - /// - /// - /// - /// - public async Task CreateEmbeddingAsync( - string input, - string? model = null, - string? user = null, - CancellationToken cancellationToken = default) - { - input = input ?? throw new ArgumentNullException(nameof(input)); - model ??= EmbeddingModelIds.Ada002; - - var response = await CreateEmbeddingAsync(new CreateEmbeddingRequest - { - Input = input, - Model = model, - User = user, - }, cancellationToken).ConfigureAwait(false); - - return response.Data.ElementAt(0).Embedding1 - .Select(static x => (float)x) - .ToArray(); - } -} diff --git a/src/libs/OpenAI/OpenAiApi.EnumsBugWorkaround.cs b/src/libs/OpenAI/OpenAiApi.EnumsBugWorkaround.cs deleted file mode 100755 index 3819f4a2..00000000 --- a/src/libs/OpenAI/OpenAiApi.EnumsBugWorkaround.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json.Serialization; - -#pragma warning disable CA1822 - -namespace tryAGI.OpenAI; - -public partial class OpenAiApi -{ - partial void UpdateJsonSerializerSettings(JsonSerializerOptions settings) - { - // Bug, does not work. - // https://github.com/RicoSuter/NSwag/issues/4460 - settings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); - } - - // ReSharper disable UnusedParameter.Local - // ReSharper disable MemberCanBeMadeStatic.Local - private async Task PrepareRequestAsync(HttpClient client, HttpRequestMessage request, string url, CancellationToken cancellationToken) - { - if (request.Content is not StringContent stringContent) - { - return; - } - -#if NET6_0_OR_GREATER - var json = await stringContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - - request.Content = new StringContent(json - .Replace(@"""role"":""System""", @"""role"":""system""", StringComparison.Ordinal) - .Replace(@"""role"":""User""", @"""role"":""user""", StringComparison.Ordinal) - .Replace(@"""role"":""Assistant""", @"""role"":""assistant""", StringComparison.Ordinal) - .Replace(@"""role"":""Function""", @"""role"":""function""", StringComparison.Ordinal) - .Replace(@"""function_call"":""Auto""", @"""function_call"":""auto""", StringComparison.Ordinal) - .Replace(@"""size"":""_256x256""", @"""size"":""256x256""", StringComparison.Ordinal) - .Replace(@"""size"":""_512x512""", @"""size"":""512x512""", StringComparison.Ordinal) - .Replace(@"""size"":""_1024x1024""", @"""size"":""1024x1024""", StringComparison.Ordinal) - .Replace(@"""response_format"":""Url""", @"""response_format"":""url""", StringComparison.Ordinal) - .Replace(@"""response_format"":""B64_json""", @"""response_format"":""b64_json""", StringComparison.Ordinal) - ); -#else - var json = await stringContent.ReadAsStringAsync().ConfigureAwait(false); - - request.Content = new StringContent(json - .Replace(@"""role"":""System""", @"""role"":""system""") - .Replace(@"""role"":""User""", @"""role"":""user""") - .Replace(@"""role"":""Assistant""", @"""role"":""assistant""") - .Replace(@"""role"":""Function""", @"""role"":""function""") - .Replace(@"""function_call"":""Auto""", @"""function_call"":""auto""") - .Replace(@"""size"":""_256x256""", @"""size"":""256x256""") - .Replace(@"""size"":""_512x512""", @"""size"":""512x512""") - .Replace(@"""size"":""_1024x1024""", @"""size"":""1024x1024""") - .Replace(@"""response_format"":""Url""", @"""response_format"":""url""") - .Replace(@"""response_format"":""B64_json""", @"""response_format"":""b64_json""") - ); -#endif - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - } - - private Task PrepareRequestAsync(HttpClient client, HttpRequestMessage request, StringBuilder builder, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - private Task ProcessResponseAsync(HttpClient client, HttpResponseMessage message, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } -} diff --git a/src/libs/OpenAI/OpenAiApi.Streaming.cs b/src/libs/OpenAI/OpenAiApi.Streaming.cs deleted file mode 100755 index ccee83fa..00000000 --- a/src/libs/OpenAI/OpenAiApi.Streaming.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; - -namespace tryAGI.OpenAI; - -public partial class OpenAiApi -{ - /// - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// - /// Creates a model response for the given chat conversation. - /// - /// OK - /// A server side error occurred. - public virtual async IAsyncEnumerable CreateChatCompletionAsStreamAsync( - CreateChatCompletionRequest body, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - body = body ?? throw new ArgumentNullException(nameof(body)); - body.Stream = true; - - var urlBuilder = new System.Text.StringBuilder(); - urlBuilder.Append(BaseUrl.TrimEnd('/')).Append("/chat/completions"); - - var url = urlBuilder.ToString(); - - using var request = new HttpRequestMessage(HttpMethod.Post, new Uri(url, UriKind.RelativeOrAbsolute)) - { - Content = new StringContent(JsonSerializer.Serialize(body, _settings.Value)) - { - Headers = - { - ContentType = MediaTypeHeaderValue.Parse("application/json"), - } - }, - Headers = - { - Accept = - { - MediaTypeWithQualityHeaderValue.Parse("text/event-stream"), - }, - } - }; - - await PrepareRequestAsync(_httpClient, request, urlBuilder, cancellationToken).ConfigureAwait(false); - await PrepareRequestAsync(_httpClient, request, url, cancellationToken).ConfigureAwait(false); - - using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - var headers = response.Headers.ToDictionary( - static h => h.Key, - static h => h.Value); - foreach (var pair in response.Content.Headers) - { - headers[pair.Key] = pair.Value; - } - - await ProcessResponseAsync(_httpClient, response, cancellationToken).ConfigureAwait(false); - - var status = (int)response.StatusCode; - if (status != 200) - { -#if NET6_0_OR_GREATER - var responseData = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); -#else - var responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false); -#endif - - throw new ApiException( - "The HTTP status code of the response was not expected (" + status + ").", - status, - responseData, - headers, - null); - } - -#if NET6_0_OR_GREATER - using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); -#else - using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); -#endif - using var reader = new StreamReader(responseStream); - - // Continuously read the stream until the end of it - while (!reader.EndOfStream) - { - cancellationToken.ThrowIfCancellationRequested(); - -#if NET7_0_OR_GREATER - var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); -#else - var line = await reader.ReadLineAsync().ConfigureAwait(false); -#endif - - // Skip empty lines - if (string.IsNullOrEmpty(line)) - { - continue; - } - - var index = line.IndexOf("{", StringComparison.Ordinal); - if (index >= 0) - { - line = line[index..]; - } - - // Exit the loop if the stream is done - if (line.StartsWith("data: [DONE]", StringComparison.OrdinalIgnoreCase)) - { - break; - } - - CreateChatCompletionStreamResponse? block; - try - { - // When the response is good, each line is a serializable CompletionCreateRequest - block = JsonSerializer.Deserialize(line); - } - catch (JsonException) - { - // When the API returns an error, it does not come back as a block, it returns a single character of text ("{"). - // In this instance, read through the rest of the response, which should be a complete object to parse. -#if NET7_0_OR_GREATER - line += await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); -#else - line += await reader.ReadToEndAsync().ConfigureAwait(false); -#endif - block = JsonSerializer.Deserialize(line); - } - - if (block == null) - { - throw new ApiException("Response was null which was not expected.", status, line, headers, null); - } - - yield return block; - } - } -}