From 9f0f96b27f8c52f7313ab46a8c2136486b5d1477 Mon Sep 17 00:00:00 2001 From: Ernie Date: Thu, 22 Feb 2024 15:32:55 -0800 Subject: [PATCH] fix text splitting to preserve original newlines etc. --- OpenAI_API/EndpointBase.cs | 794 +++++++++++++++++++------------------ 1 file changed, 405 insertions(+), 389 deletions(-) diff --git a/OpenAI_API/EndpointBase.cs b/OpenAI_API/EndpointBase.cs index d981c7e..b2e4c93 100644 --- a/OpenAI_API/EndpointBase.cs +++ b/OpenAI_API/EndpointBase.cs @@ -11,233 +11,253 @@ namespace OpenAI_API { - /// - /// A base object for any OpenAI API endpoint, encompassing common functionality - /// - public abstract class EndpointBase - { - private const string UserAgent = "okgodoit/dotnet_openai_api"; - - /// - /// The internal reference to the API, mostly used for authentication - /// - protected readonly OpenAIAPI _Api; - - /// - /// Constructor of the api endpoint base, to be called from the contructor of any devived classes. Rather than instantiating any endpoint yourself, access it through an instance of . - /// - /// - internal EndpointBase(OpenAIAPI api) - { - this._Api = api; - } - - /// - /// The name of the endpoint, which is the final path segment in the API URL. Must be overriden in a derived class. - /// - protected abstract string Endpoint { get; } - - /// - /// Gets the URL of the endpoint, based on the base OpenAI API URL followed by the endpoint name. For example "https://api.openai.com/v1/completions" - /// - protected string Url - { - get - { - return string.Format(_Api.ApiUrlFormat, _Api.ApiVersion, Endpoint); - } - } - - /// - /// Gets an HTTPClient with the appropriate authorization and other headers set - /// - /// The fully initialized HttpClient - /// Thrown if there is no valid authentication. Please refer to for details. - protected HttpClient GetClient() - { - if (_Api.Auth?.ApiKey is null) - { - throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); - } - - HttpClient client; - var clientFactory = _Api.HttpClientFactory; - if (clientFactory != null) - { - client = clientFactory.CreateClient(); - } - else - { - client = new HttpClient(); - } - - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _Api.Auth.ApiKey); - // Further authentication-header used for Azure openAI service - client.DefaultRequestHeaders.Add("api-key", _Api.Auth.ApiKey); - client.DefaultRequestHeaders.Add("User-Agent", UserAgent); - if (!string.IsNullOrEmpty(_Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", _Api.Auth.OpenAIOrganization); - - return client; - } - - /// - /// Formats a human-readable error message relating to calling the API and parsing the response - /// - /// The full content returned in the http response - /// The http response object itself - /// The name of the endpoint being used - /// Additional details about the endpoint of this request (optional) - /// A human-readable string error message. - protected string GetErrorMessage(string resultAsString, HttpResponseMessage response, string name, string description = "") - { - return $"Error at {name} ({description}) with HTTP status code: {response.StatusCode}. Content: {resultAsString ?? ""}"; - } - - - /// - /// Sends an HTTP request and returns the response. Does not do any parsing, but does do error handling. - /// - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. - /// (optional) A json-serializable object to include in the request body. - /// (optional) If true, streams the response. Otherwise waits for the entire response before returning. - /// The HttpResponseMessage of the response, which is confirmed to be successful. - /// Throws an exception if a non-success HTTP response was returned - private async Task HttpRequestRaw(string url = null, HttpMethod verb = null, object postData = null, bool streaming = false) - { - if (string.IsNullOrEmpty(url)) - url = this.Url; - - if (verb == null) - verb = HttpMethod.Get; - - using var client = GetClient(); - - HttpResponseMessage response = null; - string resultAsString = null; - HttpRequestMessage req = new HttpRequestMessage(verb, url); - - if (postData != null) - { - if (postData is HttpContent) - { - req.Content = postData as HttpContent; - } - else - { - string jsonContent = JsonConvert.SerializeObject(postData, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); - var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); - req.Content = stringContent; - } - } - response = await client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); - - if (response.IsSuccessStatusCode) - { - return response; - } - else - { - try - { - resultAsString = await response.Content.ReadAsStringAsync(); - } - catch (Exception readError) - { - resultAsString = "Additionally, the following error was thrown when attemping to read the response content: " + readError.ToString(); - } - - if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - throw new AuthenticationException("OpenAI rejected your authorization, most likely due to an invalid API Key. Try checking your API Key and see https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for guidance. Full API response follows: " + resultAsString); - } - else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) - { - throw new HttpRequestException("OpenAI had an internal server error, which can happen occasionally. Please retry your request. " + GetErrorMessage(resultAsString, response, Endpoint, url)); - } - else - { - var errorToThrow = new HttpRequestException(GetErrorMessage(resultAsString, response, Endpoint, url)); - - var parsedError = JsonConvert.DeserializeObject(resultAsString); - try - { - errorToThrow.Data.Add("message", parsedError.Error.Message); - errorToThrow.Data.Add("type", parsedError.Error.ErrorType); - errorToThrow.Data.Add("param", parsedError.Error.Parameter); - errorToThrow.Data.Add("code", parsedError.Error.ErrorCode); - } - catch (Exception parsingError) - { - throw new HttpRequestException(errorToThrow.Message, parsingError); - } - throw errorToThrow; - } - } - } - - /// - /// Sends an HTTP Get request and return the string content of the response without parsing, and does error handling. - /// - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. - /// (optional) A json-serializable object to include in the request body. - /// The text string of the response, which is confirmed to be successful. - /// Throws an exception if a non-success HTTP response was returned - internal async Task HttpGetContent(string url = null, HttpMethod verb = null, object postData = null) - { - var response = await HttpRequestRaw(url, verb, postData); - return await response.Content.ReadAsStringAsync(); - } - - /// - /// Sends an HTTP request and return the raw content stream of the response without parsing, and does error handling. - /// - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. - /// (optional) A json-serializable object to include in the request body. - /// The response content stream, which is confirmed to be successful. - /// Throws an exception if a non-success HTTP response was returned - internal async Task HttpRequest(string url = null, HttpMethod verb = null, object postData = null) - { - var response = await HttpRequestRaw(url, verb, postData); - return await response.Content.ReadAsStreamAsync(); - } - - - /// - /// Sends an HTTP Request and does initial parsing - /// - /// The -derived class for the result - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. - /// (optional) A json-serializable object to include in the request body. - /// An awaitable Task with the parsed result of type - /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. - private async Task HttpRequest(string url = null, HttpMethod verb = null, object postData = null) where T : ApiResultBase - { - var response = await HttpRequestRaw(url, verb, postData); - string resultAsString = await response.Content.ReadAsStringAsync(); - - var res = JsonConvert.DeserializeObject(resultAsString); - try - { - res.Organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); - res.RequestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); - res.ProcessingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); - res.OpenaiVersion = response.Headers.GetValues("Openai-Version").FirstOrDefault(); - if (string.IsNullOrEmpty(res.Model)) - res.Model = response.Headers.GetValues("Openai-Model").FirstOrDefault(); - } - catch (Exception e) - { - Debug.Print($"Issue parsing metadata of OpenAi Response. Url: {url}, Error: {e.ToString()}, Response: {resultAsString}. This is probably ignorable."); - } - - return res; - } - - /* + /// + /// A base object for any OpenAI API endpoint, encompassing common functionality + /// + public abstract class EndpointBase + { + private const string UserAgent = "okgodoit/dotnet_openai_api"; + + /// + /// The internal reference to the API, mostly used for authentication + /// + protected readonly OpenAIAPI _Api; + + /// + /// Constructor of the api endpoint base, to be called from the contructor of any devived classes. Rather than instantiating any endpoint yourself, access it through an instance of . + /// + /// + internal EndpointBase(OpenAIAPI api) + { + this._Api = api; + } + + /// + /// The name of the endpoint, which is the final path segment in the API URL. Must be overriden in a derived class. + /// + protected abstract string Endpoint { get; } + + /// + /// Gets the URL of the endpoint, based on the base OpenAI API URL followed by the endpoint name. For example "https://api.openai.com/v1/completions" + /// + protected string Url + { + get + { + return string.Format(_Api.ApiUrlFormat, _Api.ApiVersion, Endpoint); + } + } + + /// + /// Gets an HTTPClient with the appropriate authorization and other headers set + /// + /// The fully initialized HttpClient + /// Thrown if there is no valid authentication. Please refer to for details. + protected HttpClient GetClient() + { + if (_Api.Auth?.ApiKey is null) + { + throw new AuthenticationException("You must provide API authentication. Please refer to https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for details."); + } + + HttpClient client; + var clientFactory = _Api.HttpClientFactory; + if (clientFactory != null) + { + client = clientFactory.CreateClient(); + } + else + { + client = new HttpClient(); + } + + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _Api.Auth.ApiKey); + // Further authentication-header used for Azure openAI service + client.DefaultRequestHeaders.Add("api-key", _Api.Auth.ApiKey); + client.DefaultRequestHeaders.Add("User-Agent", UserAgent); + if (!string.IsNullOrEmpty(_Api.Auth.OpenAIOrganization)) client.DefaultRequestHeaders.Add("OpenAI-Organization", _Api.Auth.OpenAIOrganization); + + return client; + } + + /// + /// Formats a human-readable error message relating to calling the API and parsing the response + /// + /// The full content returned in the http response + /// The http response object itself + /// The name of the endpoint being used + /// Additional details about the endpoint of this request (optional) + /// A human-readable string error message. + protected string GetErrorMessage(string resultAsString, HttpResponseMessage response, string name, string description = "") + { + return $"Error at {name} ({description}) with HTTP status code: {response.StatusCode}. Content: {resultAsString ?? ""}"; + } + + /// + /// Sends an HTTP request and returns the response. Does not do any parsing, but does do error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// (optional) If true, streams the response. Otherwise waits for the entire response before returning. + /// The HttpResponseMessage of the response, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + private async Task HttpRequestRaw(string url = null, HttpMethod verb = null, object postData = null, bool streaming = false) + { + if (string.IsNullOrEmpty(url)) + url = this.Url; + + if (verb == null) + verb = HttpMethod.Get; + + using var client = GetClient(); + + HttpResponseMessage response = null; + string resultAsString = null; + HttpRequestMessage req = new HttpRequestMessage(verb, url); + + if (postData != null) + { + if (postData is HttpContent) + { + req.Content = postData as HttpContent; + } + else + { + string jsonContent = JsonConvert.SerializeObject(postData, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); + var stringContent = new StringContent(jsonContent, UnicodeEncoding.UTF8, "application/json"); + req.Content = stringContent; + } + } + response = await client.SendAsync(req, streaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); + + if (response.IsSuccessStatusCode) + { + return response; + } + else + { + try + { + resultAsString = await response.Content.ReadAsStringAsync(); + } + catch (Exception readError) + { + resultAsString = "Additionally, the following error was thrown when attemping to read the response content: " + readError.ToString(); + } + + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + throw new AuthenticationException("OpenAI rejected your authorization, most likely due to an invalid API Key. Try checking your API Key and see https://github.com/OkGoDoIt/OpenAI-API-dotnet#authentication for guidance. Full API response follows: " + resultAsString); + } + else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + { + throw new HttpRequestException("OpenAI had an internal server error, which can happen occasionally. Please retry your request. " + GetErrorMessage(resultAsString, response, Endpoint, url)); + } + else + { + var errorToThrow = new HttpRequestException(GetErrorMessage(resultAsString, response, Endpoint, url)); + ApiErrorResponse? parsedError; + try + { + parsedError = JsonConvert.DeserializeObject(resultAsString); + } + catch (Exception ex) + { + //typically gateway timeout 504 + //"\r\n504 Gateway Time-out\r\n\r\n

504 Gateway Time-out

\r\n
openresty/1.21.4.2
\r\n\r\n\r\n" + if (resultAsString == "\r\n504 Gateway Time-out\r\n\r\n

504 Gateway Time-out

\r\n
openresty/1.21.4.2
\r\n\r\n\r\n") + { + parsedError = new ApiErrorResponse(); + parsedError.Error = new ApiErrorResponseError(); + parsedError.Error.Message = "504 Gateway Time-out"; + parsedError.Error.ErrorType = "Gateway Time-out"; + parsedError.Error.Parameter = "N/A"; + parsedError.Error.ErrorCode = "504"; + } + else + { + var a = 3; + throw ex; + } + } + try + { + errorToThrow.Data.Add("message", parsedError.Error.Message); + errorToThrow.Data.Add("type", parsedError.Error.ErrorType); + errorToThrow.Data.Add("param", parsedError.Error.Parameter); + errorToThrow.Data.Add("code", parsedError.Error.ErrorCode); + } + catch (Exception parsingError) + { + throw new HttpRequestException(errorToThrow.Message, parsingError); + } + throw errorToThrow; + } + } + } + + /// + /// Sends an HTTP Get request and return the string content of the response without parsing, and does error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// The text string of the response, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + internal async Task HttpGetContent(string url = null, HttpMethod verb = null, object postData = null) + { + var response = await HttpRequestRaw(url, verb, postData); + return await response.Content.ReadAsStringAsync(); + } + + /// + /// Sends an HTTP request and return the raw content stream of the response without parsing, and does error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// The response content stream, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + internal async Task HttpRequest(string url = null, HttpMethod verb = null, object postData = null) + { + var response = await HttpRequestRaw(url, verb, postData); + return await response.Content.ReadAsStreamAsync(); + } + + /// + /// Sends an HTTP Request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + private async Task HttpRequest(string url = null, HttpMethod verb = null, object postData = null) where T : ApiResultBase + { + var response = await HttpRequestRaw(url, verb, postData); + string resultAsString = await response.Content.ReadAsStringAsync(); + + var res = JsonConvert.DeserializeObject(resultAsString); + try + { + res.Organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); + res.RequestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); + res.ProcessingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); + res.OpenaiVersion = response.Headers.GetValues("Openai-Version").FirstOrDefault(); + if (string.IsNullOrEmpty(res.Model)) + res.Model = response.Headers.GetValues("Openai-Model").FirstOrDefault(); + } + catch (Exception e) + { + Debug.Print($"Issue parsing metadata of OpenAi Response. Url: {url}, Error: {e.ToString()}, Response: {resultAsString}. This is probably ignorable."); + } + + return res; + } + + /* /// /// Sends an HTTP Request, supporting a streaming response /// @@ -271,61 +291,58 @@ private async Task StreamingHttpRequest(string url = null, HttpMethod verb } */ - /// - /// Sends an HTTP Get request and does initial parsing - /// - /// The -derived class for the result - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// An awaitable Task with the parsed result of type - /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. - internal async Task HttpGet(string url = null) where T : ApiResultBase - { - return await HttpRequest(url, HttpMethod.Get); - } - - /// - /// Sends an HTTP Post request and does initial parsing - /// - /// The -derived class for the result - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) A json-serializable object to include in the request body. - /// An awaitable Task with the parsed result of type - /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. - internal async Task HttpPost(string url = null, object postData = null) where T : ApiResultBase - { - return await HttpRequest(url, HttpMethod.Post, postData); - } - - /// - /// Sends an HTTP Delete request and does initial parsing - /// - /// The -derived class for the result - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) A json-serializable object to include in the request body. - /// An awaitable Task with the parsed result of type - /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. - internal async Task HttpDelete(string url = null, object postData = null) where T : ApiResultBase - { - return await HttpRequest(url, HttpMethod.Delete, postData); - } - - - /// - /// Sends an HTTP Put request and does initial parsing - /// - /// The -derived class for the result - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) A json-serializable object to include in the request body. - /// An awaitable Task with the parsed result of type - /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. - internal async Task HttpPut(string url = null, object postData = null) where T : ApiResultBase - { - return await HttpRequest(url, HttpMethod.Put, postData); - } - - - - /* + /// + /// Sends an HTTP Get request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpGet(string url = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Get); + } + + /// + /// Sends an HTTP Post request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpPost(string url = null, object postData = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Post, postData); + } + + /// + /// Sends an HTTP Delete request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpDelete(string url = null, object postData = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Delete, postData); + } + + /// + /// Sends an HTTP Put request and does initial parsing + /// + /// The -derived class for the result + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) A json-serializable object to include in the request body. + /// An awaitable Task with the parsed result of type + /// Throws an exception if a non-success HTTP response was returned or if the result couldn't be parsed. + internal async Task HttpPut(string url = null, object postData = null) where T : ApiResultBase + { + return await HttpRequest(url, HttpMethod.Put, postData); + } + + /* /// /// Sends an HTTP request and handles a streaming response. Does basic line splitting and error handling. /// @@ -359,111 +376,110 @@ private async IAsyncEnumerable HttpStreamingRequestRaw(string url = null } */ - - /// - /// Sends an HTTP request and handles a streaming response. Does basic line splitting and error handling. - /// - /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. - /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. - /// (optional) A json-serializable object to include in the request body. - /// The HttpResponseMessage of the response, which is confirmed to be successful. - /// Throws an exception if a non-success HTTP response was returned - protected async IAsyncEnumerable HttpStreamingRequest(string url = null, HttpMethod verb = null, object postData = null) where T : ApiResultBase - { - var response = await HttpRequestRaw(url, verb, postData, true); - - string organization = null; - string requestId = null; - TimeSpan processingTime = TimeSpan.Zero; - string openaiVersion = null; - string modelFromHeaders = null; - - try - { - organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); - requestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); - processingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); - openaiVersion = response.Headers.GetValues("Openai-Version").FirstOrDefault(); - modelFromHeaders = response.Headers.GetValues("Openai-Model").FirstOrDefault(); - } - catch (Exception e) - { - Debug.Print($"Issue parsing metadata of OpenAi Response. Url: {url}, Error: {e.ToString()}. This is probably ignorable."); - } - - string resultAsString = ""; - - using (var stream = await response.Content.ReadAsStreamAsync()) - using (StreamReader reader = new StreamReader(stream)) - { - string line; - while ((line = await reader.ReadLineAsync()) != null) - { - resultAsString += line + Environment.NewLine; - - if (line.StartsWith("data:")) - line = line.Substring("data:".Length); - - line = line.TrimStart(); - - if (line == "[DONE]") - { - yield break; - } - else if (line.StartsWith(":")) - { } - else if (!string.IsNullOrWhiteSpace(line)) - { - var res = JsonConvert.DeserializeObject(line); - - res.Organization = organization; - res.RequestId = requestId; - res.ProcessingTime = processingTime; - res.OpenaiVersion = openaiVersion; - if (string.IsNullOrEmpty(res.Model)) - res.Model = modelFromHeaders; - - yield return res; - } - } - } - } - - internal class ApiErrorResponse - { - /// - /// The error details - /// - [JsonProperty("error")] - public ApiErrorResponseError Error { get; set; } - } - internal class ApiErrorResponseError - { - /// - /// The error message - /// - [JsonProperty("message")] - - public string Message { get; set; } - - /// - /// The type of error - /// - [JsonProperty("type")] - public string ErrorType { get; set; } - - /// - /// The parameter that caused the error - /// - [JsonProperty("param")] - - public string Parameter { get; set; } - - /// - /// The error code - /// - [JsonProperty("code")] - public string ErrorCode { get; set; } - } - } + /// + /// Sends an HTTP request and handles a streaming response. Does basic line splitting and error handling. + /// + /// (optional) If provided, overrides the url endpoint for this request. If omitted, then will be used. + /// (optional) The HTTP verb to use, for example "". If omitted, then "GET" is assumed. + /// (optional) A json-serializable object to include in the request body. + /// The HttpResponseMessage of the response, which is confirmed to be successful. + /// Throws an exception if a non-success HTTP response was returned + protected async IAsyncEnumerable HttpStreamingRequest(string url = null, HttpMethod verb = null, object postData = null) where T : ApiResultBase + { + var response = await HttpRequestRaw(url, verb, postData, true); + + string organization = null; + string requestId = null; + TimeSpan processingTime = TimeSpan.Zero; + string openaiVersion = null; + string modelFromHeaders = null; + + try + { + organization = response.Headers.GetValues("Openai-Organization").FirstOrDefault(); + requestId = response.Headers.GetValues("X-Request-ID").FirstOrDefault(); + processingTime = TimeSpan.FromMilliseconds(int.Parse(response.Headers.GetValues("Openai-Processing-Ms").First())); + openaiVersion = response.Headers.GetValues("Openai-Version").FirstOrDefault(); + modelFromHeaders = response.Headers.GetValues("Openai-Model").FirstOrDefault(); + } + catch (Exception e) + { + Debug.Print($"Issue parsing metadata of OpenAi Response. Url: {url}, Error: {e.ToString()}. This is probably ignorable."); + } + + string resultAsString = ""; + + using (var stream = await response.Content.ReadAsStreamAsync()) + using (StreamReader reader = new StreamReader(stream)) + { + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + resultAsString += line + Environment.NewLine; + + if (line.StartsWith("data:")) + line = line.Substring("data:".Length); + + line = line.TrimStart(); + + if (line == "[DONE]") + { + yield break; + } + else if (line.StartsWith(":")) + { } + else if (!string.IsNullOrWhiteSpace(line)) + { + var res = JsonConvert.DeserializeObject(line); + + res.Organization = organization; + res.RequestId = requestId; + res.ProcessingTime = processingTime; + res.OpenaiVersion = openaiVersion; + if (string.IsNullOrEmpty(res.Model)) + res.Model = modelFromHeaders; + + yield return res; + } + } + } + } + + internal class ApiErrorResponse + { + /// + /// The error details + /// + [JsonProperty("error")] + public ApiErrorResponseError Error { get; set; } + } + internal class ApiErrorResponseError + { + /// + /// The error message + /// + [JsonProperty("message")] + + public string Message { get; set; } + + /// + /// The type of error + /// + [JsonProperty("type")] + public string ErrorType { get; set; } + + /// + /// The parameter that caused the error + /// + [JsonProperty("param")] + + public string Parameter { get; set; } + + /// + /// The error code + /// + [JsonProperty("code")] + public string ErrorCode { get; set; } + } + } }