diff --git a/src/ChatGPT.Net/ChatGPT.cs b/src/ChatGPT.Net/ChatGPT.cs index f29d747..957efae 100644 --- a/src/ChatGPT.Net/ChatGPT.cs +++ b/src/ChatGPT.Net/ChatGPT.cs @@ -1,11 +1,7 @@ -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Text; -using System.Text.Json; -using ChatGPT.Net.DTO; -using ChatGPT.Net.DTO.ChatGPT; -using ChatGPT.Net.DTO.ChatGPTUnofficial; +using ChatGPT.Net.DTO.ChatGPT; using Newtonsoft.Json; +using System.Net.Http.Headers; +using System.Net.Http.Json; namespace ChatGPT.Net; @@ -15,20 +11,49 @@ public class ChatGpt public ChatGptOptions Config { get; set; } = new(); public List Conversations { get; set; } = new(); public string APIKey { get; set; } + /// + /// Allows adjusting httpclient timeout for arbitrary long or short content + /// Default 100 second + /// + public static int MaxTimeout { get; set; } - public ChatGpt(string apikey, ChatGptOptions? config = null) + public ChatGpt(string apikey, + ChatGptOptions? config = null, + int maxTimeout = 100) { Config = config ?? new ChatGptOptions(); SessionId = Guid.NewGuid(); APIKey = apikey; + MaxTimeout = maxTimeout; + } + + /// + /// Each HttpClient initialization will take up additional ports and will not immediately release itself when the program stops, leading to resource waste. + /// https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines + /// + private static HttpClient _httpClient = null; + public static HttpClient httpClient + { + get + { + if (_httpClient == null) + { + _httpClient = new() + { + Timeout = TimeSpan.FromSeconds(MaxTimeout) + }; + } + + return _httpClient; + } } private async IAsyncEnumerable StreamCompletion(Stream stream) { - using var reader = new StreamReader(stream); + using StreamReader reader = new(stream); while (!reader.EndOfStream) { - var line = await reader.ReadLineAsync(); + string? line = await reader.ReadLineAsync(); if (line != null) { yield return line; @@ -38,7 +63,7 @@ private async IAsyncEnumerable StreamCompletion(Stream stream) public void SetConversationSystemMessage(string conversationId, string message) { - var conversation = GetConversation(conversationId); + ChatGptConversation conversation = GetConversation(conversationId); conversation.Messages.Add(new ChatGptMessage { Role = "system", @@ -48,7 +73,7 @@ public void SetConversationSystemMessage(string conversationId, string message) public void ReplaceConversationSystemMessage(string conversationId, string message) { - var conversation = GetConversation(conversationId); + ChatGptConversation conversation = GetConversation(conversationId); conversation.Messages = conversation.Messages.Where(x => x.Role != "system").ToList(); conversation.Messages.Add(new ChatGptMessage { @@ -56,13 +81,13 @@ public void ReplaceConversationSystemMessage(string conversationId, string messa Content = message }); } - + public void RemoveConversationSystemMessages(string conversationId, string message) { - var conversation = GetConversation(conversationId); + ChatGptConversation conversation = GetConversation(conversationId); conversation.Messages = conversation.Messages.Where(x => x.Role != "system").ToList(); } - + public List GetConversations() { return Conversations; @@ -80,7 +105,7 @@ public ChatGptConversation GetConversation(string? conversationId) return new ChatGptConversation(); } - var conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptConversation? conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conversation != null) return conversation; conversation = new ChatGptConversation() @@ -91,10 +116,10 @@ public ChatGptConversation GetConversation(string? conversationId) return conversation; } - + public void SetConversation(string conversationId, ChatGptConversation conversation) { - var conv = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptConversation? conv = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conv != null) { @@ -105,10 +130,10 @@ public void SetConversation(string conversationId, ChatGptConversation conversat Conversations.Add(conversation); } } - + public void RemoveConversation(string conversationId) { - var conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptConversation? conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conversation != null) { @@ -118,7 +143,7 @@ public void RemoveConversation(string conversationId) public void ResetConversation(string conversationId) { - var conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptConversation? conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conversation == null) return; conversation.Messages = new(); @@ -131,15 +156,15 @@ public void ClearConversations() public async Task Ask(string prompt, string? conversationId = null) { - var conversation = GetConversation(conversationId); + ChatGptConversation conversation = GetConversation(conversationId); conversation.Messages.Add(new ChatGptMessage { Role = "user", Content = prompt }); - - var reply = await SendMessage(new ChatGptRequest + + ChatGptResponse reply = await SendMessage(new ChatGptRequest { Messages = conversation.Messages, Model = Config.Model, @@ -151,10 +176,10 @@ public async Task Ask(string prompt, string? conversationId = null) Stop = Config.Stop, MaxTokens = Config.MaxTokens, }); - + conversation.Updated = DateTime.Now; - var response = reply.Choices.FirstOrDefault()?.Message.Content ?? ""; + string response = reply.Choices.FirstOrDefault()?.Message.Content ?? ""; conversation.Messages.Add(new ChatGptMessage { @@ -167,7 +192,7 @@ public async Task Ask(string prompt, string? conversationId = null) public async Task AskStream(Action callback, string prompt, string? conversationId = null) { - var conversation = GetConversation(conversationId); + ChatGptConversation conversation = GetConversation(conversationId); conversation.Messages.Add(new ChatGptMessage { @@ -175,7 +200,7 @@ public async Task AskStream(Action callback, string prompt, stri Content = prompt }); - var reply = await SendMessage(new ChatGptRequest + ChatGptResponse reply = await SendMessage(new ChatGptRequest { Messages = conversation.Messages, Model = Config.Model, @@ -188,11 +213,11 @@ public async Task AskStream(Action callback, string prompt, stri MaxTokens = Config.MaxTokens, }, response => { - var content = response.Choices.FirstOrDefault()?.Delta.Content; + string? content = response.Choices.FirstOrDefault()?.Delta.Content; if (content is null) return; if (!string.IsNullOrWhiteSpace(content)) callback(content); }); - + conversation.Updated = DateTime.Now; return reply.Choices.FirstOrDefault()?.Message.Content ?? ""; @@ -200,8 +225,8 @@ public async Task AskStream(Action callback, string prompt, stri public async Task SendMessage(ChatGptRequest requestBody, Action? callback = null) { - var client = new HttpClient(); - var request = new HttpRequestMessage + HttpClient client = ChatGpt.httpClient; + HttpRequestMessage request = new() { Method = HttpMethod.Post, RequestUri = new Uri($"{Config.BaseUrl}/v1/chat/completions"), @@ -218,34 +243,34 @@ public async Task SendMessage(ChatGptRequest requestBody, Actio } }; - var response = await client.SendAsync(request,HttpCompletionOption.ResponseHeadersRead); + HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); if (requestBody.Stream) { - var contentType = response.Content.Headers.ContentType?.MediaType; + string? contentType = response.Content.Headers.ContentType?.MediaType; if (contentType != "text/event-stream") { - var error = await response.Content.ReadFromJsonAsync(); + ChatGptResponse? error = await response.Content.ReadFromJsonAsync(); throw new Exception(error?.Error?.Message ?? "Unknown error"); } - var concatMessages = string.Empty; - + string concatMessages = string.Empty; + ChatGptStreamChunkResponse? reply = null; - var stream = await response.Content.ReadAsStreamAsync(); - await foreach (var data in StreamCompletion(stream)) + Stream stream = await response.Content.ReadAsStreamAsync(); + await foreach (string data in StreamCompletion(stream)) { - var jsonString = data.Replace("data: ", ""); + string jsonString = data.Replace("data: ", ""); if (string.IsNullOrWhiteSpace(jsonString)) continue; - if(jsonString == "[DONE]") break; + if (jsonString == "[DONE]") break; reply = JsonConvert.DeserializeObject(jsonString); if (reply is null) continue; concatMessages += reply.Choices.FirstOrDefault()?.Delta.Content; callback?.Invoke(reply); } - + return new ChatGptResponse { Id = reply?.Id ?? Guid.NewGuid().ToString(), @@ -264,9 +289,9 @@ public async Task SendMessage(ChatGptRequest requestBody, Actio }; } - var content = await response.Content.ReadFromJsonAsync(); - if(content is null) throw new Exception("Unknown error"); - if(content.Error is not null) throw new Exception(content.Error.Message); + ChatGptResponse? content = await response.Content.ReadFromJsonAsync(); + if (content is null) throw new Exception("Unknown error"); + if (content.Error is not null) throw new Exception(content.Error.Message); return content; } } diff --git a/src/ChatGPT.Net/ChatGPTUnofficial.cs b/src/ChatGPT.Net/ChatGPTUnofficial.cs index db6405b..b787e77 100644 --- a/src/ChatGPT.Net/ChatGPTUnofficial.cs +++ b/src/ChatGPT.Net/ChatGPTUnofficial.cs @@ -1,11 +1,10 @@ -using System.Net.Http.Headers; +using ChatGPT.Net.DTO; +using ChatGPT.Net.DTO.ChatGPTUnofficial; +using Newtonsoft.Json; +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; using System.Text.Json; -using ChatGPT.Net.DTO; -using ChatGPT.Net.DTO.ChatGPT; -using ChatGPT.Net.DTO.ChatGPTUnofficial; -using Newtonsoft.Json; namespace ChatGPT.Net; @@ -26,11 +25,8 @@ public ChatGptUnofficial(string sessionToken, ChatGptUnofficialOptions? config = public async Task RefreshAccessToken() { - var client = new HttpClient(new HttpClientHandler - { - UseCookies = false, - }); - var request = new HttpRequestMessage + HttpClient client = ChatGpt.httpClient; + HttpRequestMessage request = new() { Method = HttpMethod.Get, RequestUri = new Uri($"{Config.BaseUrl}/api/auth/session"), @@ -42,30 +38,30 @@ public async Task RefreshAccessToken() } }; - var response = await client.SendAsync(request); + HttpResponseMessage response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadFromJsonAsync(); + ChatGptUnofficialProfile? content = await response.Content.ReadFromJsonAsync(); const string name = "__Secure-next-auth.session-token="; - var cookies = response.Headers.GetValues("Set-Cookie"); - var sToken = cookies.FirstOrDefault(x => x.StartsWith(name)); + IEnumerable cookies = response.Headers.GetValues("Set-Cookie"); + string? sToken = cookies.FirstOrDefault(x => x.StartsWith(name)); SessionToken = sToken == null ? SessionToken : sToken.Replace(name, ""); if (content is not null) { - if(content.Error is null) AccessToken = content.AccessToken; + if (content.Error is null) AccessToken = content.AccessToken; } } private async IAsyncEnumerable StreamCompletion(Stream stream) { - using var reader = new StreamReader(stream); + using StreamReader reader = new(stream); while (!reader.EndOfStream) { - var line = await reader.ReadLineAsync(); + string? line = await reader.ReadLineAsync(); if (line != null) { yield return line; @@ -90,7 +86,7 @@ public ChatGptUnofficialConversation GetConversation(string? conversationId) return new ChatGptUnofficialConversation(); } - var conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptUnofficialConversation? conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conversation != null) return conversation; conversation = new ChatGptUnofficialConversation @@ -101,10 +97,10 @@ public ChatGptUnofficialConversation GetConversation(string? conversationId) return conversation; } - + public void SetConversation(string conversationId, ChatGptUnofficialConversation conversation) { - var conv = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptUnofficialConversation? conv = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conv != null) { @@ -115,10 +111,10 @@ public void SetConversation(string conversationId, ChatGptUnofficialConversation Conversations.Add(conversation); } } - + public void RemoveConversation(string conversationId) { - var conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptUnofficialConversation? conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conversation != null) { @@ -128,7 +124,7 @@ public void RemoveConversation(string conversationId) public void ResetConversation(string conversationId) { - var conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); + ChatGptUnofficialConversation? conversation = Conversations.FirstOrDefault(x => x.Id == conversationId); if (conversation == null) return; conversation.ParentMessageId = Guid.NewGuid().ToString(); @@ -142,16 +138,16 @@ public void ClearConversations() public async Task Ask(string prompt, string? conversationId = null) { - var conversation = GetConversation(conversationId); + ChatGptUnofficialConversation conversation = GetConversation(conversationId); - var reply = await SendMessage(prompt, Guid.NewGuid().ToString(), conversation.ParentMessageId, conversation.ConversationId, Config.Model); + ChatGptUnofficialMessageResponse reply = await SendMessage(prompt, Guid.NewGuid().ToString(), conversation.ParentMessageId, conversation.ConversationId, Config.Model); - if(reply.ConversationId is not null) + if (reply.ConversationId is not null) { conversation.ConversationId = reply.ConversationId; } - if(reply.Message.Id is not null) + if (reply.Message.Id is not null) { conversation.ParentMessageId = reply.Message.Id; } @@ -163,21 +159,21 @@ public async Task Ask(string prompt, string? conversationId = null) public async Task AskStream(Action callback, string prompt, string? conversationId = null) { - var conversation = GetConversation(conversationId); + ChatGptUnofficialConversation conversation = GetConversation(conversationId); - var reply = await SendMessage(prompt, Guid.NewGuid().ToString(), conversation.ParentMessageId, conversation.ConversationId, Config.Model, + ChatGptUnofficialMessageResponse reply = await SendMessage(prompt, Guid.NewGuid().ToString(), conversation.ParentMessageId, conversation.ConversationId, Config.Model, response => { - var content = response.Message.Content.Parts.FirstOrDefault(); + string? content = response.Message.Content.Parts.FirstOrDefault(); if (content is not null) callback(content); }); - if(reply.ConversationId is not null) + if (reply.ConversationId is not null) { conversation.ConversationId = reply.ConversationId; } - if(reply.Message.Id is not null) + if (reply.Message.Id is not null) { conversation.ParentMessageId = reply.Message.Id; } @@ -187,32 +183,35 @@ public async Task AskStream(Action callback, string prompt, stri return reply.Message.Content.Parts.FirstOrDefault() ?? ""; } - private bool ValidateToken(string token) { - if (string.IsNullOrWhiteSpace(token)) { + private bool ValidateToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { return false; } - - var tokenParts = token.Split('.'); - if (tokenParts.Length != 3) { + + string[] tokenParts = token.Split('.'); + if (tokenParts.Length != 3) + { return false; } - + //Ensure the string length is a multiple of 4 - var tokenPart = tokenParts[1]; - var remainderLength = tokenPart.Length % 4; + string tokenPart = tokenParts[1]; + int remainderLength = tokenPart.Length % 4; if (remainderLength > 0) tokenPart = tokenPart.PadRight(tokenPart.Length - remainderLength + 4, '='); - var decodedPayload = Encoding.UTF8.GetString(Convert.FromBase64String(tokenPart)); - var parsed = JsonDocument.Parse(decodedPayload).RootElement; - + string decodedPayload = Encoding.UTF8.GetString(Convert.FromBase64String(tokenPart)); + JsonElement parsed = JsonDocument.Parse(decodedPayload).RootElement; + return DateTimeOffset.Now.ToUnixTimeMilliseconds() <= parsed.GetProperty("exp").GetInt64() * 1000; } public async Task SendMessage(string message, string messageId, string? parentMessageId = null, string? conversationId = null, string? model = null, Action? callback = null) { - if(!ValidateToken(AccessToken)) await RefreshAccessToken(); - var requestData = new ChatGptUnofficialMessageRequest + if (!ValidateToken(AccessToken)) await RefreshAccessToken(); + ChatGptUnofficialMessageRequest requestData = new() { Messages = new List { @@ -228,24 +227,24 @@ public async Task SendMessage(string message, } } }; - + if (model is not null) { requestData.Model = model; } - + if (conversationId is not null) { requestData.ConversationId = conversationId; } - + if (parentMessageId is not null) { requestData.ParentMessageId = parentMessageId; } - var client = new HttpClient(); - var request = new HttpRequestMessage + HttpClient client = ChatGpt.httpClient; + HttpRequestMessage request = new() { Method = HttpMethod.Post, RequestUri = new Uri($"{Config.BaseUrl}/backend-api/conversation"), @@ -262,17 +261,17 @@ public async Task SendMessage(string message, } }; - var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); - var stream = await response.Content.ReadAsStreamAsync(); - + Stream stream = await response.Content.ReadAsStreamAsync(); + ChatGptUnofficialMessageResponse? reply = null; - await foreach (var data in StreamCompletion(stream)) + await foreach (string data in StreamCompletion(stream)) { - var dataJson = data; + string dataJson = data; //Ignore ping event if (dataJson.StartsWith("event: ")) continue; @@ -285,7 +284,7 @@ public async Task SendMessage(string message, //Try Deserialize try { - var replyNew = JsonConvert.DeserializeObject(dataJson); + ChatGptUnofficialMessageResponse? replyNew = JsonConvert.DeserializeObject(dataJson); if (replyNew == null) continue; reply = replyNew; @@ -296,4 +295,7 @@ public async Task SendMessage(string message, return reply ?? new ChatGptUnofficialMessageResponse(); } + + + } \ No newline at end of file