From b459a6426ba4fa4958ec21375167f1960fc44360 Mon Sep 17 00:00:00 2001
From: Stephen Hodgson <hodgson.designs@gmail.com>
Date: Sun, 25 Feb 2024 12:35:13 -0500
Subject: [PATCH 1/7] OpenAI-DotNet 7.7.1

- More Function utilities and invoking methods
  - Added FunctionPropertyAttribute to help better inform the feature how to format the Function json
  - Added FromFunc<,> overloads for convenance
  - Fixed invoke args sometimes being casting to wrong type
  - Added additional protections for static and instanced function calls
  - Added additional tool utilities:
    - Tool.ClearRegisteredTools
    - Tool.IsToolRegistered(Tool) - Tool.TryRegisterTool(Tool)
---
 ...cs => TestFixture_00_01_Authentication.cs} |   4 +-
 .../TestFixture_00_02_Extensions.cs           |  40 ---
 .../TestFixture_00_02_Tools.cs                |  80 +++++
 OpenAI-DotNet-Tests/TestFixture_03_Chat.cs    |  19 +-
 OpenAI-DotNet-Tests/TestFixture_12_Threads.cs |  11 +-
 .../Assistants/AssistantExtensions.cs         |   8 +-
 .../Assistants/CreateAssistantRequest.cs      |   9 +-
 .../Audio/AudioTranscriptionRequest.cs        |   4 +-
 .../Audio/AudioTranslationRequest.cs          |   4 +-
 OpenAI-DotNet/Audio/SpeechRequest.cs          |  15 +
 .../Authentication/OpenAIAuthentication.cs    |   2 +-
 OpenAI-DotNet/Chat/ChatEndpoint.cs            |   2 +-
 OpenAI-DotNet/Chat/Delta.cs                   |   2 +-
 .../{Threads => Common}/AnnotationType.cs     |   0
 OpenAI-DotNet/Common/Function.cs              | 242 +++++++++++---
 .../Common/FunctionPropertyAttribute.cs       |  53 +++
 OpenAI-DotNet/Common/Tool.cs                  | 310 +++++++++++++++++-
 .../Embeddings/EmbeddingsEndpoint.cs          |   7 +-
 OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs |   2 +-
 OpenAI-DotNet/Extensions/TypeExtensions.cs    | 102 +++++-
 .../Images/AbstractBaseImageRequest.cs        |  20 +-
 OpenAI-DotNet/Images/ImageEditRequest.cs      |  34 +-
 .../Images/ImageGenerationRequest.cs          |  24 +-
 OpenAI-DotNet/Images/ImageResult.cs           |   3 +-
 OpenAI-DotNet/Images/ImageVariationRequest.cs |  28 +-
 OpenAI-DotNet/Images/ImagesEndpoint.cs        | 107 ------
 OpenAI-DotNet/Models/Model.cs                 |   4 +-
 OpenAI-DotNet/Models/ModelsEndpoint.cs        |  11 +-
 OpenAI-DotNet/OpenAI-DotNet.csproj            |  11 +-
 29 files changed, 883 insertions(+), 275 deletions(-)
 rename OpenAI-DotNet-Tests/{TestFixture_00_00_Authentication.cs => TestFixture_00_01_Authentication.cs} (98%)
 delete mode 100644 OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs
 create mode 100644 OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs
 rename OpenAI-DotNet/{Threads => Common}/AnnotationType.cs (100%)
 create mode 100644 OpenAI-DotNet/Common/FunctionPropertyAttribute.cs

diff --git a/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs
similarity index 98%
rename from OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs
rename to OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs
index ede548af..2b03f857 100644
--- a/OpenAI-DotNet-Tests/TestFixture_00_00_Authentication.cs
+++ b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs
@@ -8,7 +8,7 @@
 
 namespace OpenAI.Tests
 {
-    internal class TestFixture_00_00_Authentication
+    internal class TestFixture_00_01_Authentication
     {
         [SetUp]
         public void Setup()
@@ -187,6 +187,8 @@ public void TearDown()
             }
 
             Assert.IsFalse(File.Exists(".openai"));
+            
+            OpenAIAuthentication.Default = null;
         }
     }
 }
diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs
deleted file mode 100644
index 52bd32c1..00000000
--- a/OpenAI-DotNet-Tests/TestFixture_00_02_Extensions.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Licensed under the MIT License. See LICENSE in the project root for license information.
-
-using NUnit.Framework;
-using System;
-
-namespace OpenAI.Tests
-{
-    internal class TestFixture_00_02_Extensions
-    {
-        [Test]
-        public void Test_01_Tools()
-        {
-            var tools = Tool.GetAllAvailableTools();
-
-            for (var i = 0; i < tools.Count; i++)
-            {
-                var tool = tools[i];
-
-                if (tool.Type != "function")
-                {
-                    Console.Write($"  \"{tool.Type}\"");
-                }
-                else
-                {
-                    Console.Write($"  \"{tool.Function.Name}\"");
-                }
-
-                if (tool.Function?.Parameters != null)
-                {
-                    Console.Write($": {tool.Function.Parameters}");
-                }
-
-                if (i < tools.Count - 1)
-                {
-                    Console.Write(",\n");
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs
new file mode 100644
index 00000000..f5748552
--- /dev/null
+++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs
@@ -0,0 +1,80 @@
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using OpenAI.Images;
+using OpenAI.Tests.Weather;
+
+namespace OpenAI.Tests
+{
+    internal class TestFixture_00_02_Tools : AbstractTestFixture
+    {
+        [Test]
+        public void Test_01_GetTools()
+        {
+            var tools = Tool.GetAllAvailableTools(forceUpdate: true, clearCache: true).ToList();
+            Assert.IsNotNull(tools);
+            Assert.IsNotEmpty(tools);
+            tools.Add(Tool.GetOrCreateTool(OpenAIClient.ImagesEndPoint, nameof(ImagesEndpoint.GenerateImageAsync)));
+            var json = JsonSerializer.Serialize(tools, new JsonSerializerOptions(OpenAIClient.JsonSerializationOptions)
+            {
+                WriteIndented = true
+            });
+            Console.WriteLine(json);
+        }
+
+        [Test]
+        public async Task Test_02_Tool_Funcs()
+        {
+            var tools = new List<Tool>
+            {
+                Tool.FromFunc("test_func", Function),
+                Tool.FromFunc<string, string, string>("test_func_with_args", FunctionWithArgs),
+                Tool.FromFunc("test_func_weather", () => WeatherService.GetCurrentWeatherAsync("my location", WeatherService.WeatherUnit.Celsius))
+            };
+
+            var json = JsonSerializer.Serialize(tools, new JsonSerializerOptions(OpenAIClient.JsonSerializationOptions)
+            {
+                WriteIndented = true
+            });
+            Console.WriteLine(json);
+            Assert.IsNotNull(tools);
+            var tool = tools[0];
+            Assert.IsNotNull(tool);
+            var result = tool.InvokeFunction<string>();
+            Assert.AreEqual("success", result);
+            var toolWithArgs = tools[1];
+            Assert.IsNotNull(toolWithArgs);
+            toolWithArgs.Function.Arguments = new JsonObject
+            {
+                ["arg1"] = "arg1",
+                ["arg2"] = "arg2"
+            };
+            var resultWithArgs = toolWithArgs.InvokeFunction<string>();
+            Assert.AreEqual("arg1 arg2", resultWithArgs);
+
+            var toolWeather = tools[2];
+            Assert.IsNotNull(toolWeather);
+            var resultWeather = await toolWeather.InvokeFunctionAsync();
+            Assert.IsFalse(string.IsNullOrWhiteSpace(resultWeather));
+            Console.WriteLine(resultWeather);
+        }
+
+        private string Function()
+        {
+            return "success";
+        }
+
+        private string FunctionWithArgs(string arg1, string arg2)
+        {
+            return $"{arg1} {arg2}";
+        }
+    }
+}
\ No newline at end of file
diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs
index c7fddc48..7c9d3711 100644
--- a/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs
+++ b/OpenAI-DotNet-Tests/TestFixture_03_Chat.cs
@@ -133,7 +133,7 @@ public async Task Test_02_01_GetChatToolCompletion()
 
             var messages = new List<Message>
             {
-                new(Role.System, "You are a helpful weather assistant."),
+                new(Role.System, "You are a helpful weather assistant.\n\r- Always prompt the user for their location."),
                 new(Role.User, "What's the weather like today?"),
             };
 
@@ -142,12 +142,13 @@ public async Task Test_02_01_GetChatToolCompletion()
                 Console.WriteLine($"{message.Role}: {message.Content}");
             }
 
-            var tools = Tool.GetAllAvailableTools(false);
+            var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
             var chatRequest = new ChatRequest(messages, tools: tools, toolChoice: "auto");
             var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
             Assert.IsNotNull(response);
             Assert.IsNotNull(response.Choices);
             Assert.IsTrue(response.Choices.Count == 1);
+            Assert.IsTrue(response.FirstChoice.FinishReason == "stop");
             messages.Add(response.FirstChoice.Message);
 
             Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");
@@ -163,7 +164,7 @@ public async Task Test_02_01_GetChatToolCompletion()
             Assert.IsTrue(response.Choices.Count == 1);
             messages.Add(response.FirstChoice.Message);
 
-            if (!string.IsNullOrEmpty(response.ToString()))
+            if (response.FirstChoice.FinishReason == "stop")
             {
                 Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");
 
@@ -198,7 +199,7 @@ public async Task Test_02_02_GetChatToolCompletion_Streaming()
             Assert.IsNotNull(OpenAIClient.ChatEndpoint);
             var messages = new List<Message>
             {
-                new(Role.System, "You are a helpful weather assistant."),
+                new(Role.System, "You are a helpful weather assistant.\n\r- Always prompt the user for their location."),
                 new(Role.User, "What's the weather like today?"),
             };
 
@@ -281,11 +282,11 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming()
             Assert.IsNotNull(OpenAIClient.ChatEndpoint);
             var messages = new List<Message>
             {
-                new(Role.System, "You are a helpful weather assistant. Use the appropriate unit based on geographical location."),
+                new(Role.System, "You are a helpful weather assistant.\n\r - Use the appropriate unit based on geographical location."),
                 new(Role.User, "What's the weather like today in Los Angeles, USA and Tokyo, Japan?"),
             };
 
-            var tools = Tool.GetAllAvailableTools(false);
+            var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
             var chatRequest = new ChatRequest(messages, model: "gpt-4-turbo-preview", tools: tools, toolChoice: "auto");
             var response = await OpenAIClient.ChatEndpoint.StreamCompletionAsync(chatRequest, partialResponse =>
             {
@@ -294,6 +295,7 @@ public async Task Test_02_03_ChatCompletion_Multiple_Tools_Streaming()
                 Assert.NotZero(partialResponse.Choices.Count);
             });
 
+            Assert.IsTrue(response.FirstChoice.FinishReason == "tool_calls");
             messages.Add(response.FirstChoice.Message);
 
             var toolCalls = response.FirstChoice.Message.ToolCalls;
@@ -328,12 +330,13 @@ public async Task Test_02_04_GetChatToolForceCompletion()
                 Console.WriteLine($"{message.Role}: {message.Content}");
             }
 
-            var tools = Tool.GetAllAvailableTools(false);
+            var tools = Tool.GetAllAvailableTools(false, forceUpdate: true, clearCache: true);
             var chatRequest = new ChatRequest(messages, tools: tools);
             var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
             Assert.IsNotNull(response);
             Assert.IsNotNull(response.Choices);
             Assert.IsTrue(response.Choices.Count == 1);
+            Assert.IsTrue(response.FirstChoice.FinishReason == "stop");
             messages.Add(response.FirstChoice.Message);
 
             Console.WriteLine($"{response.FirstChoice.Message.Role}: {response.FirstChoice} | Finish Reason: {response.FirstChoice.FinishReason}");
@@ -422,7 +425,7 @@ public async Task Test_04_01_GetChatLogProbs()
                 new(Role.Assistant, "The Los Angeles Dodgers won the World Series in 2020."),
                 new(Role.User, "Where was it played?"),
             };
-            var chatRequest = new ChatRequest(messages, Model.GPT3_5_Turbo, topLogProbs: 1);
+            var chatRequest = new ChatRequest(messages, topLogProbs: 1);
             var response = await OpenAIClient.ChatEndpoint.GetCompletionAsync(chatRequest);
             Assert.IsNotNull(response);
             Assert.IsNotNull(response.Choices);
diff --git a/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs
index c3d67f9c..2b4ce62b 100644
--- a/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs
+++ b/OpenAI-DotNet-Tests/TestFixture_12_Threads.cs
@@ -381,14 +381,9 @@ public async Task Test_07_01_SubmitToolOutput()
             Assert.IsTrue(toolCall.FunctionCall.Name.Contains(nameof(WeatherService.GetCurrentWeatherAsync)));
             Assert.IsNotNull(toolCall.FunctionCall.Arguments);
             Console.WriteLine($"tool call arguments: {toolCall.FunctionCall.Arguments}");
-            var toolOutputs = await testAssistant.GetToolOutputsAsync(run.RequiredAction.SubmitToolOutputs.ToolCalls);
-
-            foreach (var toolOutput in toolOutputs)
-            {
-                Console.WriteLine($"tool call output: {toolOutput.Output}");
-            }
-
-            run = await run.SubmitToolOutputsAsync(toolOutputs);
+            var toolOutput = await testAssistant.GetToolOutputAsync(toolCall);
+            Console.WriteLine($"tool call output: {toolOutput.Output}");
+            run = await run.SubmitToolOutputsAsync(toolOutput);
             // waiting while run in Queued and InProgress
             run = await run.WaitForStatusChangeAsync();
             Assert.AreEqual(RunStatus.Completed, run.Status);
diff --git a/OpenAI-DotNet/Assistants/AssistantExtensions.cs b/OpenAI-DotNet/Assistants/AssistantExtensions.cs
index 7c28f109..51a699ef 100644
--- a/OpenAI-DotNet/Assistants/AssistantExtensions.cs
+++ b/OpenAI-DotNet/Assistants/AssistantExtensions.cs
@@ -31,7 +31,7 @@ public static async Task<AssistantResponse> ModifyAsync(this AssistantResponse a
         /// </summary>
         /// <param name="assistant"><see cref="AssistantResponse"/>.</param>
         /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
-        /// <returns>True, if the assistant was successfully deleted.</returns>
+        /// <returns>True, if the <see cref="assistant"/> was successfully deleted.</returns>
         public static async Task<bool> DeleteAsync(this AssistantResponse assistant, CancellationToken cancellationToken = default)
             => await assistant.Client.AssistantsEndpoint.DeleteAssistantAsync(assistant.Id, cancellationToken).ConfigureAwait(false);
 
@@ -58,7 +58,7 @@ public static async Task<ListResponse<AssistantFileResponse>> ListFilesAsync(thi
             => await assistant.Client.AssistantsEndpoint.ListFilesAsync(assistant.Id, query, cancellationToken).ConfigureAwait(false);
 
         /// <summary>
-        /// Attach a file to the assistant.
+        /// Attach a file to the  <see cref="assistant"/>.
         /// </summary>
         /// <param name="assistant"><see cref="AssistantResponse"/>.</param>
         /// <param name="file">
@@ -71,7 +71,7 @@ public static async Task<AssistantFileResponse> AttachFileAsync(this AssistantRe
             => await assistant.Client.AssistantsEndpoint.AttachFileAsync(assistant.Id, file, cancellationToken).ConfigureAwait(false);
 
         /// <summary>
-        /// Uploads a new file at the specified path and attaches it to the assistant.
+        /// Uploads a new file at the specified <see cref="filePath"/> and attaches it to the <see cref="assistant"/>.
         /// </summary>
         /// <param name="assistant"><see cref="AssistantResponse"/>.</param>
         /// <param name="filePath">The local file path to upload.</param>
@@ -162,7 +162,7 @@ public static async Task<bool> DeleteFileAsync(this AssistantFileResponse file,
         }
 
         /// <summary>
-        /// Removes and Deletes a file from the assistant.
+        /// Removes and Deletes a file from the <see cref="assistant"/>.
         /// </summary>
         /// <param name="assistant"><see cref="AssistantResponse"/>.</param>
         /// <param name="fileId">The ID of the file to delete.</param>
diff --git a/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs
index ad42867d..da20605c 100644
--- a/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs
+++ b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs
@@ -45,7 +45,14 @@ public sealed class CreateAssistantRequest
         /// Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long.
         /// </param>
         public CreateAssistantRequest(AssistantResponse assistant, string model = null, string name = null, string description = null, string instructions = null, IEnumerable<Tool> tools = null, IEnumerable<string> files = null, IReadOnlyDictionary<string, string> metadata = null)
-            : this(string.IsNullOrWhiteSpace(model) ? assistant.Model : model, string.IsNullOrWhiteSpace(name) ? assistant.Name : name, string.IsNullOrWhiteSpace(description) ? assistant.Description : description, string.IsNullOrWhiteSpace(instructions) ? assistant.Instructions : instructions, tools ?? assistant.Tools, files ?? assistant.FileIds, metadata ?? assistant.Metadata)
+            : this(
+                string.IsNullOrWhiteSpace(model) ? assistant.Model : model,
+                string.IsNullOrWhiteSpace(name) ? assistant.Name : name,
+                string.IsNullOrWhiteSpace(description) ? assistant.Description : description,
+                string.IsNullOrWhiteSpace(instructions) ? assistant.Instructions : instructions,
+                tools ?? assistant.Tools,
+                files ?? assistant.FileIds,
+                metadata ?? assistant.Metadata)
         {
         }
 
diff --git a/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs b/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs
index eb643ad7..f50d8486 100644
--- a/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs
+++ b/OpenAI-DotNet/Audio/AudioTranscriptionRequest.cs
@@ -11,7 +11,7 @@ public sealed class AudioTranscriptionRequest : IDisposable
         /// Constructor.
         /// </summary>
         /// <param name="audioPath">
-        /// The audio file to transcribe, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm.
+        /// The audio file to transcribe, in one of these formats flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
         /// </param>
         /// <param name="model">
         /// ID of the model to use.
@@ -112,7 +112,7 @@ public AudioTranscriptionRequest(
         ~AudioTranscriptionRequest() => Dispose(false);
 
         /// <summary>
-        /// The audio file to transcribe, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm.
+        /// The audio file to transcribe, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
         /// </summary>
         public Stream Audio { get; }
 
diff --git a/OpenAI-DotNet/Audio/AudioTranslationRequest.cs b/OpenAI-DotNet/Audio/AudioTranslationRequest.cs
index 7a9097b1..aacb7a58 100644
--- a/OpenAI-DotNet/Audio/AudioTranslationRequest.cs
+++ b/OpenAI-DotNet/Audio/AudioTranslationRequest.cs
@@ -11,7 +11,7 @@ public sealed class AudioTranslationRequest : IDisposable
         /// Constructor.
         /// </summary>
         /// <param name="audioPath">
-        /// The audio file to translate, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm
+        /// The audio file to translate, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
         /// </param>
         /// <param name="model">
         /// ID of the model to use. Only whisper-1 is currently available.
@@ -44,7 +44,7 @@ public AudioTranslationRequest(
         /// Constructor.
         /// </summary>
         /// <param name="audio">
-        /// The audio file to translate, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm.
+        /// The audio file to translate, in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
         /// </param>
         /// <param name="audioName">
         /// The name of the audio file to translate.
diff --git a/OpenAI-DotNet/Audio/SpeechRequest.cs b/OpenAI-DotNet/Audio/SpeechRequest.cs
index 0a2f1a99..261fcf9c 100644
--- a/OpenAI-DotNet/Audio/SpeechRequest.cs
+++ b/OpenAI-DotNet/Audio/SpeechRequest.cs
@@ -25,20 +25,35 @@ public SpeechRequest(string input, Model model = null, SpeechVoice voice = Speec
             Speed = speed;
         }
 
+        /// <summary>
+        /// One of the available TTS models. Defaults to tts-1.
+        /// </summary>
         [JsonPropertyName("model")]
         public string Model { get; }
 
+        /// <summary>
+        /// The text to generate audio for. The maximum length is 4096 characters.
+        /// </summary>
         [JsonPropertyName("input")]
         public string Input { get; }
 
+        /// <summary>
+        /// The voice to use when generating the audio.
+        /// </summary>
         [JsonPropertyName("voice")]
         public SpeechVoice Voice { get; }
 
+        /// <summary>
+        /// The format to audio in. Supported formats are mp3, opus, aac, and flac.
+        /// </summary>
         [JsonPropertyName("response_format")]
         [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
         [JsonConverter(typeof(JsonStringEnumConverter<SpeechResponseFormat>))]
         public SpeechResponseFormat ResponseFormat { get; }
 
+        /// <summary>
+        /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default.
+        /// </summary>
         [JsonPropertyName("speed")]
         public float? Speed { get; }
     }
diff --git a/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs b/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs
index f6b228f7..ff245f24 100644
--- a/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs
+++ b/OpenAI-DotNet/Authentication/OpenAIAuthentication.cs
@@ -208,8 +208,8 @@ public static OpenAIAuthentication LoadFromDirectory(string directory = null, st
                                     apiKey = nextPart.Trim();
                                     break;
                                 case ORGANIZATION:
-                                case OPENAI_ORGANIZATION_ID:
                                 case OPEN_AI_ORGANIZATION_ID:
+                                case OPENAI_ORGANIZATION_ID:
                                     organization = nextPart.Trim();
                                     break;
                             }
diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs
index 6bf0c3ae..513cc892 100644
--- a/OpenAI-DotNet/Chat/ChatEndpoint.cs
+++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs
@@ -25,7 +25,7 @@ public ChatEndpoint(OpenAIClient client) : base(client) { }
         protected override string Root => "chat";
 
         /// <summary>
-        /// Creates a completion for the chat message
+        /// Creates a completion for the chat message.
         /// </summary>
         /// <param name="chatRequest">The chat request which contains the message content.</param>
         /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
diff --git a/OpenAI-DotNet/Chat/Delta.cs b/OpenAI-DotNet/Chat/Delta.cs
index e1f033e0..83639aca 100644
--- a/OpenAI-DotNet/Chat/Delta.cs
+++ b/OpenAI-DotNet/Chat/Delta.cs
@@ -47,6 +47,6 @@ public sealed class Delta
 
         public override string ToString() => Content ?? string.Empty;
 
-        public static implicit operator string(Delta delta) => delta.ToString();
+        public static implicit operator string(Delta delta) => delta?.ToString();
     }
 }
diff --git a/OpenAI-DotNet/Threads/AnnotationType.cs b/OpenAI-DotNet/Common/AnnotationType.cs
similarity index 100%
rename from OpenAI-DotNet/Threads/AnnotationType.cs
rename to OpenAI-DotNet/Common/AnnotationType.cs
diff --git a/OpenAI-DotNet/Common/Function.cs b/OpenAI-DotNet/Common/Function.cs
index aefea2dd..278de057 100644
--- a/OpenAI-DotNet/Common/Function.cs
+++ b/OpenAI-DotNet/Common/Function.cs
@@ -1,6 +1,8 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
+using OpenAI.Extensions;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Reflection;
 using System.Text.Json;
@@ -16,10 +18,10 @@ namespace OpenAI
     /// </summary>
     public sealed class Function
     {
-        public Function() { }
-
         private const string NameRegex = "^[a-zA-Z0-9_-]{1,64}$";
 
+        public Function() { }
+
         /// <summary>
         /// Creates a new function description to insert into a chat conversation.
         /// </summary>
@@ -33,10 +35,7 @@ public Function() { }
         /// <param name="parameters">
         /// An optional JSON object describing the parameters of the function that the model can generate.
         /// </param>
-        /// <param name="arguments">
-        /// An optional JSON object describing the arguments to use when invoking the function.
-        /// </param>
-        public Function(string name, string description = null, JsonNode parameters = null, JsonNode arguments = null)
+        public Function(string name, string description = null, JsonNode parameters = null)
         {
             if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex))
             {
@@ -46,12 +45,37 @@ public Function(string name, string description = null, JsonNode parameters = nu
             Name = name;
             Description = description;
             Parameters = parameters;
-            Arguments = arguments;
+            functionCache[Name] = this;
         }
 
-        internal Function(Function other) => CopyFrom(other);
+        /// <summary>
+        /// Creates a new function description to insert into a chat conversation.
+        /// </summary>
+        /// <param name="name">
+        /// Required. The name of the function to generate arguments for based on the context in a message.<br/>
+        /// May contain a-z, A-Z, 0-9, underscores and dashes, with a maximum length of 64 characters. Recommended to not begin with a number or a dash.
+        /// </param>
+        /// <param name="description">
+        /// An optional description of the function, used by the API to determine if it is useful to include in the response.
+        /// </param>
+        /// <param name="parameters">
+        /// An optional JSON describing the parameters of the function that the model can generate.
+        /// </param>
+        public Function(string name, string description, string parameters)
+        {
+            if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex))
+            {
+                throw new ArgumentException($"The name of the function does not conform to naming standards: {NameRegex}");
+            }
+
+            Name = name;
+            Description = description;
+            Parameters = JsonNode.Parse(parameters);
+            functionCache[Name] = this;
+        }
 
-        internal Function(string name, string description, JsonObject parameters, MethodInfo method)
+
+        internal Function(string name, string description, MethodInfo method, object instance = null)
         {
             if (!System.Text.RegularExpressions.Regex.IsMatch(name, NameRegex))
             {
@@ -60,10 +84,54 @@ internal Function(string name, string description, JsonObject parameters, Method
 
             Name = name;
             Description = description;
-            Parameters = parameters;
-            functionCache[Name] = method;
+            MethodInfo = method;
+            Parameters = method.GenerateJsonSchema();
+            Instance = instance;
+            functionCache[Name] = this;
         }
 
+        #region Func<,> Overloads
+
+        public static Function FromFunc<TResult>(string name, Func<TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, TResult>(string name, Func<T1, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, TResult>(string name, Func<T1, T2, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, TResult>(string name, Func<T1, T2, T3, TResult> function, string description = null)
+        => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, TResult>(string name, Func<T1, T2, T3, T4, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, T5, TResult>(string name, Func<T1, T2, T3, T4, T5, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, T5, T6, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        public static Function FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult> function, string description = null)
+            => new(name, description, function.Method, function.Target);
+
+        #endregion Func<,> Overloads
+
+        internal Function(Function other) => CopyFrom(other);
+
         /// <summary>
         /// The name of the function to generate arguments for.<br/>
         /// May contain a-z, A-Z, 0-9, and underscores and dashes, with a maximum length of 64 characters.
@@ -129,6 +197,18 @@ public JsonNode Arguments
             internal set => arguments = value;
         }
 
+        /// <summary>
+        /// The instance of the object to invoke the method on.
+        /// </summary>
+        [JsonIgnore]
+        internal object Instance { get; }
+
+        /// <summary>
+        /// The method to invoke.
+        /// </summary>
+        [JsonIgnore]
+        private MethodInfo MethodInfo { get; }
+
         internal void CopyFrom(Function other)
         {
             if (!string.IsNullOrWhiteSpace(other.Name))
@@ -154,57 +234,137 @@ internal void CopyFrom(Function other)
 
         #region Function Invoking Utilities
 
-        private static readonly Dictionary<string, MethodInfo> functionCache = new();
+        private static readonly ConcurrentDictionary<string, Function> functionCache = new();
 
+        /// <summary>
+        /// Invokes the function and returns the result as json.
+        /// </summary>
+        /// <returns>The result of the function as json.</returns>
         public string Invoke()
         {
-            var (method, invokeArgs) = ValidateFunctionArguments();
-            var result = method.Invoke(null, invokeArgs);
-            return result == null ? string.Empty : JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions);
+            try
+            {
+                var (function, invokeArgs) = ValidateFunctionArguments();
+
+                if (function.MethodInfo.ReturnType == typeof(void))
+                {
+                    function.MethodInfo.Invoke(function.Instance, invokeArgs);
+                    return "{\"result\": \"success\"}";
+                }
+
+                var result = Invoke<object>();
+                return JsonSerializer.Serialize(new { result });
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e);
+                return JsonSerializer.Serialize(new { error = e.Message });
+            }
+        }
+
+        /// <summary>
+        /// Invokes the function and returns the result.
+        /// </summary>
+        /// <typeparam name="T">The expected return type.</typeparam>
+        /// <returns>The result of the function.</returns>
+        public T Invoke<T>()
+        {
+            try
+            {
+                var (function, invokeArgs) = ValidateFunctionArguments();
+                var result = function.MethodInfo.Invoke(function.Instance, invokeArgs);
+                return result == null ? default : (T)result;
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine(e);
+                throw;
+            }
         }
 
+        /// <summary>
+        /// Invokes the function and returns the result as json.
+        /// </summary>
+        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
+        /// <returns>The result of the function as json.</returns>
         public async Task<string> InvokeAsync(CancellationToken cancellationToken = default)
         {
-            var (method, invokeArgs) = ValidateFunctionArguments(cancellationToken);
-            var task = (Task)method.Invoke(null, invokeArgs);
+            try
+            {
+                var (function, invokeArgs) = ValidateFunctionArguments(cancellationToken);
+
+                if (function.MethodInfo.ReturnType == typeof(Task))
+                {
+                    if (function.MethodInfo.Invoke(function.Instance, invokeArgs) is not Task task)
+                    {
+                        throw new InvalidOperationException($"The function {Name} did not return a valid Task.");
+                    }
+
+                    await task;
+                    return "{\"result\": \"success\"}";
+                }
 
-            if (task is null)
+                var result = await InvokeAsync<object>(cancellationToken);
+                return JsonSerializer.Serialize(new { result });
+            }
+            catch (Exception e)
             {
-                throw new InvalidOperationException($"The function {Name} did not return a Task.");
+                Console.WriteLine(e);
+                return JsonSerializer.Serialize(new { error = e.Message });
             }
+        }
+
+        /// <summary>
+        /// Invokes the function and returns the result.
+        /// </summary>
+        /// <typeparam name="T">Expected return type.</typeparam>
+        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
+        /// <returns>The result of the function.</returns>
+        public async Task<T> InvokeAsync<T>(CancellationToken cancellationToken = default)
+        {
+            try
+            {
+                var (function, invokeArgs) = ValidateFunctionArguments(cancellationToken);
 
-            await task.ConfigureAwait(false);
+                if (function.MethodInfo.Invoke(function.Instance, invokeArgs) is not Task task)
+                {
+                    throw new InvalidOperationException($"The function {Name} did not return a valid Task.");
+                }
 
-            if (method.ReturnType == typeof(Task))
+                await task;
+                // ReSharper disable once InconsistentNaming
+                const string Result = nameof(Result);
+                var resultProperty = task.GetType().GetProperty(Result);
+                return (T)resultProperty?.GetValue(task);
+            }
+            catch (Exception e)
             {
-                return string.Empty;
+                Console.WriteLine(e);
+                throw;
             }
-
-            var result = method.ReturnType.GetProperty(nameof(Task<object>.Result))?.GetValue(task);
-            return result == null ? string.Empty : JsonSerializer.Serialize(new { result }, OpenAIClient.JsonSerializationOptions);
         }
 
-        private (MethodInfo method, object[] invokeArgs) ValidateFunctionArguments(CancellationToken cancellationToken = default)
+        private (Function function, object[] invokeArgs) ValidateFunctionArguments(CancellationToken cancellationToken = default)
         {
-            if (Parameters != null && Arguments == null)
+            if (Parameters != null && Parameters.AsObject().Count > 0 && Arguments == null)
             {
                 throw new ArgumentException($"Function {Name} has parameters but no arguments are set.");
             }
 
-            if (!functionCache.TryGetValue(Name, out var method))
+            if (!functionCache.TryGetValue(Name, out var function))
             {
-                if (!Name.Contains('_'))
-                {
-                    throw new InvalidOperationException($"Failed to lookup and invoke function \"{Name}\"");
-                }
+                throw new InvalidOperationException($"Failed to find a valid function for {Name}");
+            }
 
-                var type = Type.GetType(Name[..Name.LastIndexOf('_')].Replace('_', '.')) ?? throw new InvalidOperationException($"Failed to find a valid type for {Name}");
-                method = type.GetMethod(Name[(Name.LastIndexOf('_') + 1)..].Replace('_', '.')) ?? throw new InvalidOperationException($"Failed to find a valid method for {Name}");
-                functionCache[Name] = method;
+            if (function.MethodInfo == null)
+            {
+                throw new InvalidOperationException($"Failed to find a valid method for {Name}");
             }
 
-            var requestedArgs = JsonSerializer.Deserialize<Dictionary<string, object>>(Arguments.ToString(), OpenAIClient.JsonSerializationOptions);
-            var methodParams = method.GetParameters();
+            var requestedArgs = arguments != null
+                ? JsonSerializer.Deserialize<Dictionary<string, object>>(Arguments.ToString(), OpenAIClient.JsonSerializationOptions)
+                : new();
+            var methodParams = function.MethodInfo.GetParameters();
             var invokeArgs = new object[methodParams.Length];
 
             for (var i = 0; i < methodParams.Length; i++)
@@ -213,7 +373,7 @@ public async Task<string> InvokeAsync(CancellationToken cancellationToken = defa
 
                 if (parameter.Name == null)
                 {
-                    throw new InvalidOperationException($"Failed to find a valid parameter name for {method.DeclaringType}.{method.Name}()");
+                    throw new InvalidOperationException($"Failed to find a valid parameter name for {function.MethodInfo.DeclaringType}.{function.MethodInfo.Name}()");
                 }
 
                 if (requestedArgs.TryGetValue(parameter.Name, out var value))
@@ -222,6 +382,10 @@ public async Task<string> InvokeAsync(CancellationToken cancellationToken = defa
                     {
                         invokeArgs[i] = cancellationToken;
                     }
+                    else if (value is string @enum && parameter.ParameterType.IsEnum)
+                    {
+                        invokeArgs[i] = Enum.Parse(parameter.ParameterType, @enum);
+                    }
                     else if (value is JsonElement element)
                     {
                         invokeArgs[i] = JsonSerializer.Deserialize(element.GetRawText(), parameter.ParameterType, OpenAIClient.JsonSerializationOptions);
@@ -241,7 +405,7 @@ public async Task<string> InvokeAsync(CancellationToken cancellationToken = defa
                 }
             }
 
-            return (method, invokeArgs);
+            return (function, invokeArgs);
         }
 
         #endregion Function Invoking Utilities
diff --git a/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs b/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs
new file mode 100644
index 00000000..e2d5a007
--- /dev/null
+++ b/OpenAI-DotNet/Common/FunctionPropertyAttribute.cs
@@ -0,0 +1,53 @@
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System;
+
+namespace OpenAI
+{
+    [AttributeUsage(AttributeTargets.Property)]
+    public sealed class FunctionPropertyAttribute : Attribute
+    {
+        /// <summary>
+        /// Property Attribute to help with function calling.
+        /// </summary>
+        /// <param name="description">
+        /// The description of the property
+        /// </param>
+        /// <param name="required">
+        /// Is the property required?
+        /// </param>
+        /// <param name="defaultValue">
+        /// The default value.
+        /// </param>
+        /// <param name="possibleValues">
+        /// Enums or other possible values.
+        /// </param>
+        public FunctionPropertyAttribute(string description = null, bool required = false, object defaultValue = null, params object[] possibleValues)
+        {
+            Description = description;
+            Required = required;
+            DefaultValue = defaultValue;
+            PossibleValues = possibleValues;
+        }
+
+        /// <summary>
+        /// The description of the property
+        /// </summary>
+        public string Description { get; }
+
+        /// <summary>
+        /// Is the property required?
+        /// </summary>
+        public bool Required { get; }
+
+        /// <summary>
+        /// The default value.
+        /// </summary>
+        public object DefaultValue { get; }
+
+        /// <summary>
+        /// Enums or other possible values.
+        /// </summary>
+        public object[] PossibleValues { get; }
+    }
+}
diff --git a/OpenAI-DotNet/Common/Tool.cs b/OpenAI-DotNet/Common/Tool.cs
index 92e29d5c..1ad95e34 100644
--- a/OpenAI-DotNet/Common/Tool.cs
+++ b/OpenAI-DotNet/Common/Tool.cs
@@ -1,6 +1,5 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
-using OpenAI.Extensions;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -47,11 +46,6 @@ public Tool(Function function)
         [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
         public Function Function { get; private set; }
 
-        public string InvokeFunction() => Function.Invoke();
-
-        public async Task<string> InvokeFunctionAsync(CancellationToken cancellationToken = default)
-            => await Function.InvokeAsync(cancellationToken).ConfigureAwait(false);
-
         internal void CopyFrom(Tool other)
         {
             if (!string.IsNullOrWhiteSpace(other?.Id))
@@ -82,23 +76,117 @@ internal void CopyFrom(Tool other)
             }
         }
 
-        private static List<Tool> toolCache = new()
+        /// <summary>
+        /// Invokes the function and returns the result as json.
+        /// </summary>
+        /// <returns>The result of the function as json.</returns>
+        public string InvokeFunction() => Function.Invoke();
+
+        /// <summary>
+        /// Invokes the function and returns the result.
+        /// </summary>
+        /// <typeparam name="T">The type to deserialize the result to.</typeparam>
+        /// <returns>The result of the function.</returns>
+        public T InvokeFunction<T>() => Function.Invoke<T>();
+
+        /// <summary>
+        /// Invokes the function and returns the result as json.
+        /// </summary>
+        /// <param name="cancellationToken">Optional, A token to cancel the request.</param>
+        /// <returns>The result of the function as json.</returns>
+        public async Task<string> InvokeFunctionAsync(CancellationToken cancellationToken = default)
+            => await Function.InvokeAsync(cancellationToken).ConfigureAwait(false);
+
+        /// <summary>
+        /// Invokes the function and returns the result.
+        /// </summary>
+        /// <typeparam name="T">The type to deserialize the result to.</typeparam>
+        /// <param name="cancellationToken">Optional, A token to cancel the request.</param>
+        /// <returns>The result of the function.</returns>
+        public async Task<T> InvokeFunctionAsync<T>(CancellationToken cancellationToken = default)
+            => await Function.InvokeAsync<T>(cancellationToken).ConfigureAwait(false);
+
+        private static readonly List<Tool> toolCache = new()
         {
             Retrieval,
             CodeInterpreter
         };
 
+        /// <summary>
+        /// Clears the tool cache of all previously registered tools.
+        /// </summary>
+        public static void ClearRegisteredTools()
+        {
+            toolCache.Clear();
+            toolCache.Add(CodeInterpreter);
+            toolCache.Add(Retrieval);
+        }
+
+        /// <summary>
+        /// Checks if tool exists in cache.
+        /// </summary>
+        /// <param name="tool">The tool to check.</param>
+        /// <returns>True, if the tool is already registered in the tool cache.</returns>
+        public static bool IsToolRegistered(Tool tool)
+            => toolCache.Any(knownTool =>
+                knownTool.Type == "function" &&
+                knownTool.Function.Name == tool.Function.Name &&
+                ReferenceEquals(knownTool.Function.Instance, tool.Function.Instance));
+
+        /// <summary>
+        /// Tries to register a tool into the Tool cache.
+        /// </summary>
+        /// <param name="tool">The tool to register.</param>
+        /// <returns>True, if the tool was added to the cache.</returns>
+        public static bool TryRegisterTool(Tool tool)
+        {
+            if (IsToolRegistered(tool))
+            {
+                return false;
+            }
+
+            if (tool.Type != "function")
+            {
+                throw new InvalidOperationException("Only function tools can be registered.");
+            }
+
+            toolCache.Add(tool);
+            return true;
+
+        }
+
+        private static bool TryGetTool(string name, object instance, out Tool tool)
+        {
+            foreach (var knownTool in toolCache.Where(knownTool =>
+                         knownTool.Type == "function" &&
+                         knownTool.Function.Name == name &&
+                         ReferenceEquals(knownTool, instance)))
+            {
+                tool = knownTool;
+                return true;
+            }
+
+            tool = null;
+            return false;
+        }
+
         /// <summary>
         /// Gets a list of all available tools.
         /// </summary>
         /// <remarks>
-        /// This method will scan all assemblies for methods decorated with the <see cref="FunctionAttribute"/>.
+        /// This method will scan all assemblies for static methods decorated with the <see cref="FunctionAttribute"/>.
         /// </remarks>
         /// <param name="includeDefaults">Optional, Whether to include the default tools (Retrieval and CodeInterpreter).</param>
         /// <param name="forceUpdate">Optional, Whether to force an update of the tool cache.</param>
+        /// <param name="clearCache">Optional, whether to force the tool cache to be cleared before updating.</param>
         /// <returns>A list of all available tools.</returns>
-        public static IReadOnlyList<Tool> GetAllAvailableTools(bool includeDefaults = true, bool forceUpdate = false)
+        public static IReadOnlyList<Tool> GetAllAvailableTools(bool includeDefaults = true, bool forceUpdate = false, bool clearCache = false)
         {
+            if (clearCache)
+            {
+                ClearRegisteredTools();
+            }
+
             if (forceUpdate || toolCache.All(tool => tool.Type != "function"))
             {
                 var tools = new List<Tool>();
@@ -106,16 +194,18 @@ public static IReadOnlyList<Tool> GetAllAvailableTools(bool includeDefaults = tr
                     from assembly in AppDomain.CurrentDomain.GetAssemblies()
                     from type in assembly.GetTypes()
                     from method in type.GetMethods()
+                    where method.IsStatic
                     let functionAttribute = method.GetCustomAttribute<FunctionAttribute>()
                     where functionAttribute != null
                     let name = $"{type.FullName}.{method.Name}".Replace('.', '_')
                     let description = functionAttribute.Description
-                    let parameters = method.GenerateJsonSchema()
-                    select new Function(name, description, parameters, method)
+                    select new Function(name, description, method)
                     into function
                     select new Tool(function));
 
-                foreach (var newTool in tools.Where(knownTool => !toolCache.Any(tool => tool.Type == "function" && tool.Function.Name == knownTool.Function.Name)))
+                foreach (var newTool in tools.Where(tool =>
+                             !toolCache.Any(knownTool =>
+                                 knownTool.Type == "function" && knownTool.Function.Name == tool.Function.Name && knownTool.Function.Instance == null)))
                 {
                     toolCache.Add(newTool);
                 }
@@ -127,13 +217,13 @@ into function
         }
 
         /// <summary>
-        /// Get or create a tool from a method.
+        /// Get or create a tool from a static method.
         /// </summary>
         /// <remarks>
         /// If the tool already exists, it will be returned. Otherwise, a new tool will be created.<br/>
         /// The method doesn't need to be decorated with the <see cref="FunctionAttribute"/>.<br/>
         /// </remarks>
-        /// <param name="type">The type containing the method.</param>
+        /// <param name="type">The type containing the static method.</param>
         /// <param name="methodName">The name of the method.</param>
         /// <param name="description">Optional, The description of the method.</param>
         /// <returns>The tool for the method.</returns>
@@ -141,16 +231,202 @@ public static Tool GetOrCreateTool(Type type, string methodName, string descript
         {
             var method = type.GetMethod(methodName) ??
                 throw new InvalidOperationException($"Failed to find a valid method for {type.FullName}.{methodName}()");
+
+            if (!method.IsStatic)
+            {
+                throw new InvalidOperationException($"Method {type.FullName}.{methodName}() must be static. Use GetOrCreateTool(object instance, string methodName) instead.");
+            }
+
+            var functionName = $"{type.FullName}.{method.Name}".Replace('.', '_');
+
+            if (TryGetTool(functionName, null, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(new Function(functionName, description ?? string.Empty, method));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        /// <summary>
+        /// Get or create a tool from a method of an instance of an object.
+        /// </summary>
+        /// <remarks>
+        /// If the tool already exists, it will be returned. Otherwise, a new tool will be created.<br/>
+        /// The method doesn't need to be decorated with the <see cref="FunctionAttribute"/>.<br/>
+        /// </remarks>
+        /// <param name="instance">The instance of the object containing the method.</param>
+        /// <param name="methodName">The name of the method.</param>
+        /// <param name="description">Optional, The description of the method.</param>
+        /// <returns>The tool for the method.</returns>
+        public static Tool GetOrCreateTool(object instance, string methodName, string description = null)
+        {
+            var type = instance.GetType();
+            var method = type.GetMethod(methodName) ??
+                throw new InvalidOperationException($"Failed to find a valid method for {type.FullName}.{methodName}()");
+
             var functionName = $"{type.FullName}.{method.Name}".Replace('.', '_');
 
-            foreach (var knownTool in toolCache.Where(knownTool => knownTool.Type == "function" && knownTool.Function.Name == functionName))
+            if (TryGetTool(functionName, instance, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(new Function(functionName, description ?? string.Empty, method, instance));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        #region Func<,> Overloads
+
+        public static Tool FromFunc<TResult>(string name, Func<TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
             {
-                return knownTool;
+                return tool;
             }
 
-            var tool = new Tool(new Function(functionName, description ?? string.Empty, method.GenerateJsonSchema(), method));
+            tool = new Tool(Function.FromFunc(name, function, description));
             toolCache.Add(tool);
             return tool;
         }
+
+        public static Tool FromFunc<T1, TResult>(string name, Func<T1, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, TResult>(string name, Func<T1, T2, TResult> function,
+            string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, TResult>(string name, Func<T1, T2, T3, TResult> function,
+            string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, T4, TResult>(string name, Func<T1, T2, T3, T4, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, T4, T5, TResult>(string name, Func<T1, T2, T3, T4, T5, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, T4, T5, T6, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, T4, T5, T6, T7, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        public static Tool FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+
+        public static Tool FromFunc<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult>(string name, Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult> function, string description = null)
+        {
+            if (TryGetTool(name, function, out var tool))
+            {
+                return tool;
+            }
+
+            tool = new Tool(Function.FromFunc(name, function, description));
+            toolCache.Add(tool);
+            return tool;
+        }
+
+        #endregion Func<,> Overloads
     }
 }
\ No newline at end of file
diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
index 3cdfd1ea..3aea137b 100644
--- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
+++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
@@ -39,9 +39,10 @@ public EmbeddingsEndpoint(OpenAIClient client) : base(client) { }
         /// The number of dimensions the resulting output embeddings should have.
         /// Only supported in text-embedding-3 and later models
         /// </param>
+        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
         /// <returns><see cref="EmbeddingsResponse"/></returns>
-        public async Task<EmbeddingsResponse> CreateEmbeddingAsync(string input, string model = null, string user = null, int? dimensions = null)
-            => await CreateEmbeddingAsync(new EmbeddingsRequest(input, model, user, dimensions)).ConfigureAwait(false);
+        public async Task<EmbeddingsResponse> CreateEmbeddingAsync(string input, string model = null, string user = null, int? dimensions = null, CancellationToken cancellationToken = default)
+            => await CreateEmbeddingAsync(new EmbeddingsRequest(input, model, user, dimensions), cancellationToken).ConfigureAwait(false);
 
         /// <summary>
         /// Creates an embedding vector representing the input text.
@@ -70,7 +71,7 @@ public async Task<EmbeddingsResponse> CreateEmbeddingAsync(IEnumerable<string> i
         /// <summary>
         /// Creates an embedding vector representing the input text.
         /// </summary>
-        /// <param name="request"><see cref="EmbeddingsRequest"/></param>
+        /// <param name="request"><see cref="EmbeddingsRequest"/>.</param>
         /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
         /// <returns><see cref="EmbeddingsResponse"/></returns>
         public async Task<EmbeddingsResponse> CreateEmbeddingAsync(EmbeddingsRequest request, CancellationToken cancellationToken = default)
diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs
index 1b5edf74..19383fee 100644
--- a/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs
+++ b/OpenAI-DotNet/Embeddings/EmbeddingsRequest.cs
@@ -47,7 +47,7 @@ public EmbeddingsRequest(string input, string model = null, string user = null,
         /// Each input must not exceed 8192 tokens in length.
         /// </param>
         /// <param name="model">
-        /// The model id to use.
+        /// The model id to use.<br/>
         /// Defaults to: <see cref="Model.Embedding_Ada_002"/>
         /// </param>
         /// <param name="user">
diff --git a/OpenAI-DotNet/Extensions/TypeExtensions.cs b/OpenAI-DotNet/Extensions/TypeExtensions.cs
index 91160f91..f342efc5 100644
--- a/OpenAI-DotNet/Extensions/TypeExtensions.cs
+++ b/OpenAI-DotNet/Extensions/TypeExtensions.cs
@@ -3,7 +3,10 @@
 using System;
 using System.Collections.Generic;
 using System.Reflection;
+using System.Text.Json;
 using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Threading;
 
 namespace OpenAI.Extensions
 {
@@ -11,6 +14,13 @@ internal static class TypeExtensions
     {
         public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo)
         {
+            var parameters = methodInfo.GetParameters();
+
+            if (parameters.Length == 0)
+            {
+                return null;
+            }
+
             var schema = new JsonObject
             {
                 ["type"] = "object",
@@ -18,8 +28,13 @@ public static JsonObject GenerateJsonSchema(this MethodInfo methodInfo)
             };
             var requiredParameters = new JsonArray();
 
-            foreach (var parameter in methodInfo.GetParameters())
+            foreach (var parameter in parameters)
             {
+                if (parameter.ParameterType == typeof(CancellationToken))
+                {
+                    continue;
+                }
+
                 if (string.IsNullOrWhiteSpace(parameter.Name))
                 {
                     throw new InvalidOperationException($"Failed to find a valid parameter name for {methodInfo.DeclaringType}.{methodInfo.Name}()");
@@ -52,7 +67,7 @@ public static JsonObject GenerateJsonSchema(this Type type)
 
                 foreach (var value in Enum.GetValues(type))
                 {
-                    schema["enum"].AsArray().Add(value.ToString());
+                    schema["enum"].AsArray().Add(JsonNode.Parse(JsonSerializer.Serialize(value, OpenAIClient.JsonSerializationOptions)));
                 }
             }
             else if (type.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)))
@@ -70,13 +85,63 @@ public static JsonObject GenerateJsonSchema(this Type type)
                 foreach (var property in properties)
                 {
                     var propertyInfo = GenerateJsonSchema(property.PropertyType);
+                    var functionPropertyAttribute = property.GetCustomAttribute<FunctionPropertyAttribute>();
+                    var jsonPropertyAttribute = property.GetCustomAttribute<JsonPropertyNameAttribute>();
+                    var propertyName = jsonPropertyAttribute?.Name ?? property.Name;
 
-                    if (Nullable.GetUnderlyingType(property.PropertyType) == null)
+                    // override properties with values from function property attribute
+                    if (functionPropertyAttribute != null)
+                    {
+                        propertyInfo["description"] = functionPropertyAttribute.Description;
+
+                        if (functionPropertyAttribute.Required)
+                        {
+                            requiredProperties.Add(propertyName);
+                        }
+
+                        JsonNode defaultValue = null;
+
+                        if (functionPropertyAttribute.DefaultValue != null)
+                        {
+                            defaultValue = JsonNode.Parse(JsonSerializer.Serialize(functionPropertyAttribute.DefaultValue, OpenAIClient.JsonSerializationOptions));
+                            propertyInfo["default"] = defaultValue;
+                        }
+
+                        if (functionPropertyAttribute.PossibleValues is { Length: > 0 })
+                        {
+                            var enums = new JsonArray();
+
+                            foreach (var value in functionPropertyAttribute.PossibleValues)
+                            {
+                                var @enum = JsonNode.Parse(JsonSerializer.Serialize(value, OpenAIClient.JsonSerializationOptions));
+
+                                if (defaultValue == null)
+                                {
+                                    enums.Add(@enum);
+                                }
+                                else
+                                {
+                                    if (@enum != defaultValue)
+                                    {
+                                        enums.Add(@enum);
+                                    }
+                                }
+                            }
+
+                            if (defaultValue != null && !enums.Contains(defaultValue))
+                            {
+                                enums.Add(JsonNode.Parse(defaultValue.ToJsonString()));
+                            }
+
+                            propertyInfo["enum"] = enums;
+                        }
+                    }
+                    else if (Nullable.GetUnderlyingType(property.PropertyType) == null)
                     {
-                        requiredProperties.Add(property.Name);
+                        requiredProperties.Add(propertyName);
                     }
 
-                    propertiesInfo[property.Name] = propertyInfo;
+                    propertiesInfo[propertyName] = propertyInfo;
                 }
 
                 schema["properties"] = propertiesInfo;
@@ -88,7 +153,32 @@ public static JsonObject GenerateJsonSchema(this Type type)
             }
             else
             {
-                schema["type"] = type.Name.ToLower();
+                if (type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte))
+                {
+                    schema["type"] = "integer";
+                }
+                else if (type == typeof(float) || type == typeof(double) || type == typeof(decimal))
+                {
+                    schema["type"] = "number";
+                }
+                else if (type == typeof(bool))
+                {
+                    schema["type"] = "boolean";
+                }
+                else if (type == typeof(DateTime) || type == typeof(DateTimeOffset))
+                {
+                    schema["type"] = "string";
+                    schema["format"] = "date-time";
+                }
+                else if (type == typeof(Guid))
+                {
+                    schema["type"] = "string";
+                    schema["format"] = "uuid";
+                }
+                else
+                {
+                    schema["type"] = type.Name.ToLower();
+                }
             }
 
             return schema;
diff --git a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs
index cf9b3599..a31a7174 100644
--- a/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs
+++ b/OpenAI-DotNet/Images/AbstractBaseImageRequest.cs
@@ -1,6 +1,7 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
 using OpenAI.Extensions;
+using OpenAI.Models;
 using System;
 using System.Text.Json.Serialization;
 
@@ -14,6 +15,9 @@ public abstract class AbstractBaseImageRequest
         /// <summary>
         /// Constructor.
         /// </summary>
+        /// <param name="model">
+        /// The model to use for image generation.
+        /// </param>
         /// <param name="numberOfResults">
         /// The number of images to generate. Must be between 1 and 10.
         /// </param>
@@ -29,10 +33,10 @@ public abstract class AbstractBaseImageRequest
         /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
         /// </param>
         /// <exception cref="ArgumentOutOfRangeException"></exception>
-        protected AbstractBaseImageRequest(int numberOfResults = 1, ImageSize size = ImageSize.Large, ResponseFormat responseFormat = ResponseFormat.Url, string user = null)
+        protected AbstractBaseImageRequest(Model model = null, int numberOfResults = 1, ImageSize size = ImageSize.Large, ResponseFormat responseFormat = ResponseFormat.Url, string user = null)
         {
+            Model = string.IsNullOrWhiteSpace(model?.Id) ? Models.Model.DallE_2 : model;
             Number = numberOfResults;
-
             Size = size switch
             {
                 ImageSize.Small => "256x256",
@@ -40,15 +44,22 @@ protected AbstractBaseImageRequest(int numberOfResults = 1, ImageSize size = Ima
                 ImageSize.Large => "1024x1024",
                 _ => throw new ArgumentOutOfRangeException(nameof(size), size, null)
             };
-
             User = user;
             ResponseFormat = responseFormat;
         }
 
+        /// <summary>
+        /// The model to use for image generation.
+        /// </summary>
+        [JsonPropertyName("model")]
+        [FunctionProperty("The model to use for image generation.", true, "dall-e-2")]
+        public string Model { get; }
+
         /// <summary>
         /// The number of images to generate. Must be between 1 and 10.
         /// </summary>
         [JsonPropertyName("n")]
+        [FunctionProperty("The number of images to generate. Must be between 1 and 10.", false, 1)]
         public int Number { get; }
 
         /// <summary>
@@ -58,18 +69,21 @@ protected AbstractBaseImageRequest(int numberOfResults = 1, ImageSize size = Ima
         /// </summary>
         [JsonPropertyName("response_format")]
         [JsonConverter(typeof(JsonStringEnumConverter<ResponseFormat>))]
+        [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.")]
         public ResponseFormat ResponseFormat { get; }
 
         /// <summary>
         /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024.
         /// </summary>
         [JsonPropertyName("size")]
+        [FunctionProperty("The size of the generated images.", false, "256x256", "512x512", "1024x1024")]
         public string Size { get; }
 
         /// <summary>
         /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
         /// </summary>
         [JsonPropertyName("user")]
+        [FunctionProperty("A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.")]
         public string User { get; }
     }
 }
\ No newline at end of file
diff --git a/OpenAI-DotNet/Images/ImageEditRequest.cs b/OpenAI-DotNet/Images/ImageEditRequest.cs
index d298b623..ef5e6f05 100644
--- a/OpenAI-DotNet/Images/ImageEditRequest.cs
+++ b/OpenAI-DotNet/Images/ImageEditRequest.cs
@@ -1,5 +1,6 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
+using OpenAI.Models;
 using System;
 using System.IO;
 
@@ -31,14 +32,18 @@ public sealed class ImageEditRequest : AbstractBaseImageRequest, IDisposable
         /// Must be one of url or b64_json.
         /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
         /// </param>
+        /// <param name="model">
+        /// The model to use for image generation.
+        /// </param>
         public ImageEditRequest(
             string imagePath,
             string prompt,
             int numberOfResults = 1,
             ImageSize size = ImageSize.Large,
             string user = null,
-            ResponseFormat responseFormat = ResponseFormat.Url)
-            : this(imagePath, null, prompt, numberOfResults, size, user, responseFormat)
+            ResponseFormat responseFormat = ResponseFormat.Url,
+            Model model = null)
+            : this(imagePath, null, prompt, numberOfResults, size, user, responseFormat, model)
         {
         }
 
@@ -70,6 +75,9 @@ public ImageEditRequest(
         /// Must be one of url or b64_json.
         /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
         /// </param>
+        /// <param name="model">
+        /// The model to use for image generation.
+        /// </param>
         public ImageEditRequest(
             string imagePath,
             string maskPath,
@@ -77,7 +85,8 @@ public ImageEditRequest(
             int numberOfResults = 1,
             ImageSize size = ImageSize.Large,
             string user = null,
-            ResponseFormat responseFormat = ResponseFormat.Url)
+            ResponseFormat responseFormat = ResponseFormat.Url,
+            Model model = null)
             : this(
                 File.OpenRead(imagePath),
                 Path.GetFileName(imagePath),
@@ -87,7 +96,8 @@ public ImageEditRequest(
                 numberOfResults,
                 size,
                 user,
-                responseFormat)
+                responseFormat,
+                model)
         {
         }
 
@@ -116,6 +126,9 @@ public ImageEditRequest(
         /// Must be one of url or b64_json.
         /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
         /// </param>
+        /// <param name="model">
+        /// The model to use for image generation.
+        /// </param>
         public ImageEditRequest(
             Stream image,
             string imageName,
@@ -123,8 +136,9 @@ public ImageEditRequest(
             int numberOfResults = 1,
             ImageSize size = ImageSize.Large,
             string user = null,
-            ResponseFormat responseFormat = ResponseFormat.Url)
-            : this(image, imageName, null, null, prompt, numberOfResults, size, user, responseFormat)
+            ResponseFormat responseFormat = ResponseFormat.Url,
+            Model model = null)
+            : this(image, imageName, null, null, prompt, numberOfResults, size, user, responseFormat, model)
         {
         }
 
@@ -158,6 +172,9 @@ public ImageEditRequest(
         /// Must be one of url or b64_json.
         /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
         /// </param>
+        /// <param name="model">
+        /// The model to use for image generation.
+        /// </param>
         public ImageEditRequest(
             Stream image,
             string imageName,
@@ -167,8 +184,9 @@ public ImageEditRequest(
             int numberOfResults = 1,
             ImageSize size = ImageSize.Large,
             string user = null,
-            ResponseFormat responseFormat = ResponseFormat.Url)
-            : base(numberOfResults, size, responseFormat, user)
+            ResponseFormat responseFormat = ResponseFormat.Url,
+            Model model = null)
+            : base(model, numberOfResults, size, responseFormat, user)
         {
             Image = image;
 
diff --git a/OpenAI-DotNet/Images/ImageGenerationRequest.cs b/OpenAI-DotNet/Images/ImageGenerationRequest.cs
index cca15d0b..3c93e686 100644
--- a/OpenAI-DotNet/Images/ImageGenerationRequest.cs
+++ b/OpenAI-DotNet/Images/ImageGenerationRequest.cs
@@ -1,9 +1,8 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
+using OpenAI.Extensions;
 using OpenAI.Models;
-using System;
 using System.Text.Json.Serialization;
-using OpenAI.Extensions;
 
 namespace OpenAI.Images
 {
@@ -12,12 +11,6 @@ namespace OpenAI.Images
     /// </summary>
     public sealed class ImageGenerationRequest
     {
-        [Obsolete("Use new constructor")]
-        public ImageGenerationRequest(string prompt, int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, ResponseFormat responseFormat = ResponseFormat.Url)
-        {
-            throw new NotSupportedException();
-        }
-
         /// <summary>
         /// Constructor.
         /// </summary>
@@ -72,12 +65,13 @@ public ImageGenerationRequest(
             Number = numberOfResults;
             Quality = quality;
             ResponseFormat = responseFormat;
-            Size = size;
+            Size = size ?? "1024x1024";
             Style = style;
             User = user;
         }
 
         [JsonPropertyName("model")]
+        [FunctionProperty("The model to use for image generation.", true, "dall-e-2", "dall-e-3")]
         public string Model { get; }
 
         /// <summary>
@@ -85,6 +79,7 @@ public ImageGenerationRequest(
         /// The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3.
         /// </summary>
         [JsonPropertyName("prompt")]
+        [FunctionProperty("A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3.", true)]
         public string Prompt { get; }
 
         /// <summary>
@@ -92,14 +87,18 @@ public ImageGenerationRequest(
         /// Must be between 1 and 10. For dall-e-3, only n=1 is supported.
         /// </summary>
         [JsonPropertyName("n")]
+        [FunctionProperty("The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported.", true, 1)]
         public int Number { get; }
 
         /// <summary>
         /// The quality of the image that will be generated.
+        /// Must be one of standard or hd.
         /// hd creates images with finer details and greater consistency across the image.
         /// This param is only supported for dall-e-3.
         /// </summary>
         [JsonPropertyName("quality")]
+        [FunctionProperty("The quality of the image that will be generated. hd creates images with finer details and greater consistency across the image. This param is only supported for dall-e-3.",
+            possibleValues: new object[] { "standard", "hd" })]
         public string Quality { get; }
 
         /// <summary>
@@ -109,6 +108,7 @@ public ImageGenerationRequest(
         /// </summary>
         [JsonPropertyName("response_format")]
         [JsonConverter(typeof(JsonStringEnumConverter<ResponseFormat>))]
+        [FunctionProperty("The format in which the generated images are returned. Must be one of url or b64_json.", true)]
         public ResponseFormat ResponseFormat { get; }
 
         /// <summary>
@@ -117,6 +117,9 @@ public ImageGenerationRequest(
         /// Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models.
         /// </summary>
         [JsonPropertyName("size")]
+        [FunctionProperty("The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2. Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models.", true,
+            defaultValue: "1024x1024",
+            possibleValues: new object[] { "256x256", "512x512", "1024x1024", "1792x1024", "1024x1792" })]
         public string Size { get; }
 
         /// <summary>
@@ -127,12 +130,15 @@ public ImageGenerationRequest(
         /// This param is only supported for dall-e-3.
         /// </summary>
         [JsonPropertyName("style")]
+        [FunctionProperty("The style of the generated images. Must be one of vivid or natural. Vivid causes the model to lean towards generating hyper-real and dramatic images. Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3.",
+            possibleValues: new object[] { "vivid", "natural" })]
         public string Style { get; }
 
         /// <summary>
         /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
         /// </summary>
         [JsonPropertyName("user")]
+        [FunctionProperty("A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.")]
         public string User { get; }
     }
 }
diff --git a/OpenAI-DotNet/Images/ImageResult.cs b/OpenAI-DotNet/Images/ImageResult.cs
index d47187f7..9820bad5 100644
--- a/OpenAI-DotNet/Images/ImageResult.cs
+++ b/OpenAI-DotNet/Images/ImageResult.cs
@@ -24,6 +24,7 @@ public override string ToString()
             => !string.IsNullOrWhiteSpace(Url)
                 ? Url
                 : !string.IsNullOrWhiteSpace(B64_Json)
-                    ? B64_Json : null;
+                    ? B64_Json
+                    : string.Empty;
     }
 }
diff --git a/OpenAI-DotNet/Images/ImageVariationRequest.cs b/OpenAI-DotNet/Images/ImageVariationRequest.cs
index a4948e82..34db426c 100644
--- a/OpenAI-DotNet/Images/ImageVariationRequest.cs
+++ b/OpenAI-DotNet/Images/ImageVariationRequest.cs
@@ -1,5 +1,6 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
+using OpenAI.Models;
 using System;
 using System.IO;
 
@@ -27,8 +28,17 @@ public sealed class ImageVariationRequest : AbstractBaseImageRequest, IDisposabl
         /// Must be one of url or b64_json.
         /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
         /// </param>
-        public ImageVariationRequest(string imagePath, int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, ResponseFormat responseFormat = ResponseFormat.Url)
-            : this(File.OpenRead(imagePath), Path.GetFileName(imagePath), numberOfResults, size, user, responseFormat)
+        /// <param name="model">
+        /// The model to use for image generation.
+        /// </param>
+        public ImageVariationRequest(
+            string imagePath,
+            int numberOfResults = 1,
+            ImageSize size = ImageSize.Large,
+            string user = null,
+            ResponseFormat responseFormat = ResponseFormat.Url,
+            Model model = null)
+            : this(File.OpenRead(imagePath), Path.GetFileName(imagePath), numberOfResults, size, user, responseFormat, model)
         {
         }
 
@@ -55,8 +65,18 @@ public ImageVariationRequest(string imagePath, int numberOfResults = 1, ImageSiz
         /// Must be one of url or b64_json.
         /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
         /// </param>
-        public ImageVariationRequest(Stream image, string imageName, int numberOfResults = 1, ImageSize size = ImageSize.Large, string user = null, ResponseFormat responseFormat = ResponseFormat.Url)
-            : base(numberOfResults, size, responseFormat, user)
+        /// <param name="model">
+        /// The model to use for image generation.
+        /// </param>
+        public ImageVariationRequest(
+            Stream image,
+            string imageName,
+            int numberOfResults = 1,
+            ImageSize size = ImageSize.Large,
+            string user = null,
+            ResponseFormat responseFormat = ResponseFormat.Url,
+            Model model = null)
+            : base(model, numberOfResults, size, responseFormat, user)
         {
             Image = image;
 
diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs
index 3f6f39f6..3ed8629b 100644
--- a/OpenAI-DotNet/Images/ImagesEndpoint.cs
+++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs
@@ -1,7 +1,6 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
 using OpenAI.Extensions;
-using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Net.Http;
@@ -23,39 +22,6 @@ internal ImagesEndpoint(OpenAIClient client) : base(client) { }
         /// <inheritdoc />
         protected override string Root => "images";
 
-        /// <summary>
-        /// Creates an image given a prompt.
-        /// </summary>
-        /// <param name="prompt">
-        /// A text description of the desired image(s). The maximum length is 1000 characters.
-        /// </param>
-        /// <param name="numberOfResults">
-        /// The number of images to generate. Must be between 1 and 10.
-        /// </param>
-        /// <param name="size">
-        /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024.
-        /// </param>
-        /// <param name="user">
-        /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
-        /// </param>
-        /// <param name="responseFormat">
-        /// The format in which the generated images are returned. Must be one of url or b64_json.
-        /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
-        /// </param>
-        /// <param name="cancellationToken">
-        /// Optional, <see cref="CancellationToken"/>.
-        /// </param>
-        /// <returns>A list of generated texture urls to download.</returns>
-        [Obsolete]
-        public async Task<IReadOnlyList<ImageResult>> GenerateImageAsync(
-            string prompt,
-            int numberOfResults = 1,
-            ImageSize size = ImageSize.Large,
-            string user = null,
-            ResponseFormat responseFormat = ResponseFormat.Url,
-            CancellationToken cancellationToken = default)
-            => await GenerateImageAsync(new ImageGenerationRequest(prompt, numberOfResults, size, user, responseFormat), cancellationToken).ConfigureAwait(false);
-
         /// <summary>
         /// Creates an image given a prompt.
         /// </summary>
@@ -69,47 +35,6 @@ public async Task<IReadOnlyList<ImageResult>> GenerateImageAsync(ImageGeneration
             return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false);
         }
 
-        /// <summary>
-        /// Creates an edited or extended image given an original image and a prompt.
-        /// </summary>
-        /// <param name="image">
-        /// The image to edit. Must be a valid PNG file, less than 4MB, and square.
-        /// If mask is not provided, image must have transparency, which will be used as the mask.
-        /// </param>
-        /// <param name="mask">
-        /// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited.
-        /// Must be a valid PNG file, less than 4MB, and have the same dimensions as image.
-        /// </param>
-        /// <param name="prompt">
-        /// A text description of the desired image(s). The maximum length is 1000 characters.
-        /// </param>
-        /// <param name="numberOfResults">
-        /// The number of images to generate. Must be between 1 and 10.
-        /// </param>
-        /// <param name="size">
-        /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024.
-        /// </param>
-        /// <param name="responseFormat">
-        /// The format in which the generated images are returned. Must be one of url or b64_json.
-        /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
-        /// </param>
-        /// <param name="user">
-        /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
-        /// </param>
-        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
-        /// <returns>A list of generated texture urls to download.</returns>
-        [Obsolete("Use new constructor")]
-        public async Task<IReadOnlyList<ImageResult>> CreateImageEditAsync(
-            string image,
-            string mask,
-            string prompt,
-            int numberOfResults = 1,
-            ImageSize size = ImageSize.Large,
-            ResponseFormat responseFormat = ResponseFormat.Url,
-            string user = null,
-            CancellationToken cancellationToken = default)
-            => await CreateImageEditAsync(new ImageEditRequest(image, mask, prompt, numberOfResults, size, user, responseFormat), cancellationToken).ConfigureAwait(false);
-
         /// <summary>
         /// Creates an edited or extended image given an original image and a prompt.
         /// </summary>
@@ -145,38 +70,6 @@ public async Task<IReadOnlyList<ImageResult>> CreateImageEditAsync(ImageEditRequ
             return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false);
         }
 
-        /// <summary>
-        /// Creates a variation of a given image.
-        /// </summary>
-        /// <param name="imagePath">
-        /// The image to edit. Must be a valid PNG file, less than 4MB, and square.
-        /// If mask is not provided, image must have transparency, which will be used as the mask.
-        /// </param>
-        /// <param name="numberOfResults">
-        /// The number of images to generate. Must be between 1 and 10.
-        /// </param>
-        /// <param name="size">
-        /// The size of the generated images. Must be one of 256x256, 512x512, or 1024x1024.
-        /// </param>
-        /// <param name="responseFormat">
-        /// The format in which the generated images are returned. Must be one of url or b64_json.
-        /// <para/> Defaults to <see cref="ResponseFormat.Url"/>
-        /// </param>
-        /// <param name="user">
-        /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
-        /// </param>
-        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
-        /// <returns>A list of generated texture urls to download.</returns>
-        [Obsolete("Use new constructor")]
-        public async Task<IReadOnlyList<ImageResult>> CreateImageVariationAsync(
-            string imagePath,
-            int numberOfResults = 1,
-            ImageSize size = ImageSize.Large,
-            ResponseFormat responseFormat = ResponseFormat.Url,
-            string user = null,
-            CancellationToken cancellationToken = default)
-            => await CreateImageVariationAsync(new ImageVariationRequest(imagePath, numberOfResults, size, user, responseFormat), cancellationToken).ConfigureAwait(false);
-
         /// <summary>
         /// Creates a variation of a given image.
         /// </summary>
diff --git a/OpenAI-DotNet/Models/Model.cs b/OpenAI-DotNet/Models/Model.cs
index 835f8392..8c89cf20 100644
--- a/OpenAI-DotNet/Models/Model.cs
+++ b/OpenAI-DotNet/Models/Model.cs
@@ -130,12 +130,12 @@ public Model(string id, string ownedBy = null)
         /// The default model for <see cref="Embeddings.EmbeddingsEndpoint"/>.
         /// </summary>
         public static Model Embedding_Ada_002 { get; } = new("text-embedding-ada-002", "openai");
-        
+
         /// <summary>
         /// A highly efficient model which provides a significant upgrade over its predecessor, the text-embedding-ada-002 model.
         /// </summary>
         public static Model Embedding_3_Small { get; } = new("text-embedding-3-small", "openai");
-        
+
         /// <summary>
         /// A next generation larger model with embeddings of up to 3072 dimensions.
         /// </summary>
diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs
index db99dc74..21de5af9 100644
--- a/OpenAI-DotNet/Models/ModelsEndpoint.cs
+++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs
@@ -12,7 +12,7 @@ namespace OpenAI.Models
 {
     /// <summary>
     /// List and describe the various models available in the API.
-    /// You can refer to the Models documentation to understand what <see href="https://platform.openai.com/docs/models"/> are available and the differences between them.<br/>
+    /// You can refer to the Models documentation to understand which models are available for certain endpoints: <see href="https://platform.openai.com/docs/models/model-endpoint-compatibility"/>.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/models"/>
     /// </summary>
     public sealed class ModelsEndpoint : BaseEndPoint
@@ -33,7 +33,7 @@ public ModelsEndpoint(OpenAIClient client) : base(client) { }
         /// <summary>
         /// List all models via the API
         /// </summary>
-        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/></param>
+        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
         /// <returns>Asynchronously returns the list of all <see cref="Model"/>s</returns>
         public async Task<IReadOnlyList<Model>> GetModelsAsync(CancellationToken cancellationToken = default)
         {
@@ -46,7 +46,7 @@ public async Task<IReadOnlyList<Model>> GetModelsAsync(CancellationToken cancell
         /// Get the details about a particular Model from the API
         /// </summary>
         /// <param name="id">The id/name of the model to get more details about</param>
-        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/></param>
+        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
         /// <returns>Asynchronously returns the <see cref="Model"/> with all available properties</returns>
         public async Task<Model> GetModelDetailsAsync(string id, CancellationToken cancellationToken = default)
         {
@@ -59,13 +59,14 @@ public async Task<Model> GetModelDetailsAsync(string id, CancellationToken cance
         /// Delete a fine-tuned model. You must have the Owner role in your organization.
         /// </summary>
         /// <param name="modelId">The <see cref="Model"/> to delete.</param>
-        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/></param>
+        /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
         /// <returns>True, if fine-tuned model was successfully deleted.</returns>
         public async Task<bool> DeleteFineTuneModelAsync(string modelId, CancellationToken cancellationToken = default)
         {
             var model = await GetModelDetailsAsync(modelId, cancellationToken).ConfigureAwait(false);
 
-            if (model == null)
+            if (model == null ||
+                string.IsNullOrWhiteSpace(model))
             {
                 throw new Exception($"Failed to get {modelId} info!");
             }
diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj
index 2aebec37..0c8d259e 100644
--- a/OpenAI-DotNet/OpenAI-DotNet.csproj
+++ b/OpenAI-DotNet/OpenAI-DotNet.csproj
@@ -28,8 +28,17 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet-
     <AssemblyOriginatorKeyFile>OpenAI-DotNet.pfx</AssemblyOriginatorKeyFile>
     <IncludeSymbols>True</IncludeSymbols>
     <TreatWarningsAsErrors>True</TreatWarningsAsErrors>
-    <Version>7.7.0</Version>
+    <Version>7.7.1</Version>
     <PackageReleaseNotes>
+Version 7.7.1
+- More Function utilities and invoking methods
+  - Added FunctionPropertyAttribute to help better inform the feature how to format the Function json
+  - Added FromFunc-&gt;,-&lt; overloads for convenance
+  - Fixed invoke args sometimes being casting to wrong type
+  - Added additional protections for static and instanced function calls
+  - Added additional tool utilities:
+    - Tool.ClearRegisteredTools
+    - Tool.IsToolRegistered(Tool) - Tool.TryRegisterTool(Tool)
 Version 7.7.0
 - Added Tool call and Function call Utilities and helper methods
   - Added FunctionAttribute to decorate methods to be used in function calling

From 62940bd429ac67c4d9b5fdbed528beafe1e1ddff Mon Sep 17 00:00:00 2001
From: Stephen Hodgson <hodgson.designs@gmail.com>
Date: Sun, 25 Feb 2024 12:53:11 -0500
Subject: [PATCH 2/7] rename BaseEndPoint -> OpenAIBaseEndpoint

---
 OpenAI-DotNet/Assistants/AssistantsEndpoint.cs                | 2 +-
 OpenAI-DotNet/Audio/AudioEndpoint.cs                          | 2 +-
 OpenAI-DotNet/Chat/ChatEndpoint.cs                            | 2 +-
 .../Common/{BaseEndPoint.cs => OpenAIBaseEndpoint.cs}         | 4 ++--
 OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs                | 2 +-
 OpenAI-DotNet/Files/FilesEndpoint.cs                          | 2 +-
 OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs                | 2 +-
 OpenAI-DotNet/Images/ImagesEndpoint.cs                        | 2 +-
 OpenAI-DotNet/Models/ModelsEndpoint.cs                        | 2 +-
 OpenAI-DotNet/Moderations/ModerationsEndpoint.cs              | 2 +-
 OpenAI-DotNet/Threads/ThreadsEndpoint.cs                      | 2 +-
 11 files changed, 12 insertions(+), 12 deletions(-)
 rename OpenAI-DotNet/Common/{BaseEndPoint.cs => OpenAIBaseEndpoint.cs} (93%)

diff --git a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs
index 8596120d..8c6d86ae 100644
--- a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs
+++ b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs
@@ -9,7 +9,7 @@
 
 namespace OpenAI.Assistants
 {
-    public sealed class AssistantsEndpoint : BaseEndPoint
+    public sealed class AssistantsEndpoint : OpenAIBaseEndpoint
     {
         internal AssistantsEndpoint(OpenAIClient client) : base(client) { }
 
diff --git a/OpenAI-DotNet/Audio/AudioEndpoint.cs b/OpenAI-DotNet/Audio/AudioEndpoint.cs
index 5fcd703f..c1d40bcc 100644
--- a/OpenAI-DotNet/Audio/AudioEndpoint.cs
+++ b/OpenAI-DotNet/Audio/AudioEndpoint.cs
@@ -15,7 +15,7 @@ namespace OpenAI.Audio
     /// Transforms audio into text.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/audio"/>
     /// </summary>
-    public sealed class AudioEndpoint : BaseEndPoint
+    public sealed class AudioEndpoint : OpenAIBaseEndpoint
     {
         private class AudioResponse
         {
diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs
index 513cc892..54c5e884 100644
--- a/OpenAI-DotNet/Chat/ChatEndpoint.cs
+++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs
@@ -16,7 +16,7 @@ namespace OpenAI.Chat
     /// Given a chat conversation, the model will return a chat completion response.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/chat"/>
     /// </summary>
-    public sealed class ChatEndpoint : BaseEndPoint
+    public sealed class ChatEndpoint : OpenAIBaseEndpoint
     {
         /// <inheritdoc />
         public ChatEndpoint(OpenAIClient client) : base(client) { }
diff --git a/OpenAI-DotNet/Common/BaseEndPoint.cs b/OpenAI-DotNet/Common/OpenAIBaseEndpoint.cs
similarity index 93%
rename from OpenAI-DotNet/Common/BaseEndPoint.cs
rename to OpenAI-DotNet/Common/OpenAIBaseEndpoint.cs
index 1da86097..e785520d 100644
--- a/OpenAI-DotNet/Common/BaseEndPoint.cs
+++ b/OpenAI-DotNet/Common/OpenAIBaseEndpoint.cs
@@ -5,9 +5,9 @@
 
 namespace OpenAI
 {
-    public abstract class BaseEndPoint
+    public abstract class OpenAIBaseEndpoint
     {
-        protected BaseEndPoint(OpenAIClient client) => this.client = client;
+        protected OpenAIBaseEndpoint(OpenAIClient client) => this.client = client;
 
         // ReSharper disable once InconsistentNaming
         protected readonly OpenAIClient client;
diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
index 3aea137b..c722124f 100644
--- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
+++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
@@ -12,7 +12,7 @@ namespace OpenAI.Embeddings
     /// Get a vector representation of a given input that can be easily consumed by machine learning models and algorithms.<br/>
     /// <see href="https://platform.openai.com/docs/guides/embeddings"/>
     /// </summary>
-    public sealed class EmbeddingsEndpoint : BaseEndPoint
+    public sealed class EmbeddingsEndpoint : OpenAIBaseEndpoint
     {
         /// <inheritdoc />
         public EmbeddingsEndpoint(OpenAIClient client) : base(client) { }
diff --git a/OpenAI-DotNet/Files/FilesEndpoint.cs b/OpenAI-DotNet/Files/FilesEndpoint.cs
index 35f548f8..ae09f5f6 100644
--- a/OpenAI-DotNet/Files/FilesEndpoint.cs
+++ b/OpenAI-DotNet/Files/FilesEndpoint.cs
@@ -17,7 +17,7 @@ namespace OpenAI.Files
     /// Files are used to upload documents that can be used with features like Fine-tuning.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/files"/>
     /// </summary>
-    public sealed class FilesEndpoint : BaseEndPoint
+    public sealed class FilesEndpoint : OpenAIBaseEndpoint
     {
         private class FilesList
         {
diff --git a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs
index d64f195c..5b92fc42 100644
--- a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs
+++ b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs
@@ -14,7 +14,7 @@ namespace OpenAI.FineTuning
     /// <see href="https://platform.openai.com/docs/guides/fine-tuning"/><br/>
     /// <see href="https://platform.openai.com/docs/api-reference/fine-tuning"/>
     /// </summary>
-    public sealed class FineTuningEndpoint : BaseEndPoint
+    public sealed class FineTuningEndpoint : OpenAIBaseEndpoint
     {
         /// <inheritdoc />
         public FineTuningEndpoint(OpenAIClient client) : base(client) { }
diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs
index 3ed8629b..74ff8198 100644
--- a/OpenAI-DotNet/Images/ImagesEndpoint.cs
+++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs
@@ -14,7 +14,7 @@ namespace OpenAI.Images
     /// Given a prompt and/or an input image, the model will generate a new image.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/images"/>
     /// </summary>
-    public sealed class ImagesEndpoint : BaseEndPoint
+    public sealed class ImagesEndpoint : OpenAIBaseEndpoint
     {
         /// <inheritdoc />
         internal ImagesEndpoint(OpenAIClient client) : base(client) { }
diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs
index 21de5af9..12161f7c 100644
--- a/OpenAI-DotNet/Models/ModelsEndpoint.cs
+++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs
@@ -15,7 +15,7 @@ namespace OpenAI.Models
     /// You can refer to the Models documentation to understand which models are available for certain endpoints: <see href="https://platform.openai.com/docs/models/model-endpoint-compatibility"/>.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/models"/>
     /// </summary>
-    public sealed class ModelsEndpoint : BaseEndPoint
+    public sealed class ModelsEndpoint : OpenAIBaseEndpoint
     {
         private sealed class ModelsList
         {
diff --git a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs
index 8cc0ff22..a1edf7cc 100644
--- a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs
+++ b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs
@@ -14,7 +14,7 @@ namespace OpenAI.Moderations
     /// Developers can thus identify content that our content policy prohibits and take action, for instance by filtering it.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/moderations"/>
     /// </summary>
-    public sealed class ModerationsEndpoint : BaseEndPoint
+    public sealed class ModerationsEndpoint : OpenAIBaseEndpoint
     {
         /// <inheritdoc />
         public ModerationsEndpoint(OpenAIClient client) : base(client) { }
diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs
index c9b03f7c..8c66df2d 100644
--- a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs
+++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs
@@ -12,7 +12,7 @@ namespace OpenAI.Threads
     /// Create threads that assistants can interact with.<br/>
     /// <see href="https://platform.openai.com/docs/api-reference/threads"/>
     /// </summary>
-    public sealed class ThreadsEndpoint : BaseEndPoint
+    public sealed class ThreadsEndpoint : OpenAIBaseEndpoint
     {
         public ThreadsEndpoint(OpenAIClient client) : base(client) { }
 

From 0cde283a632428fa9eeecada8d9d0a786a55ebea Mon Sep 17 00:00:00 2001
From: Stephen Hodgson <hodgson.designs@gmail.com>
Date: Sun, 25 Feb 2024 15:07:05 -0500
Subject: [PATCH 3/7] more tweaks and fixes

---
 .../TestFixture_00_02_Tools.cs                |   2 -
 .../Assistants/AssistantsEndpoint.cs          |  42 +++----
 OpenAI-DotNet/Audio/AudioEndpoint.cs          |  15 +--
 OpenAI-DotNet/Chat/ChatEndpoint.cs            |  92 ++++++++++++---
 .../Embeddings/EmbeddingsEndpoint.cs          |   6 +-
 .../Extensions/ResponseExtensions.cs          | 109 ++++++++++++++++--
 OpenAI-DotNet/Extensions/StringExtensions.cs  |  12 +-
 OpenAI-DotNet/Files/FilesEndpoint.cs          |  18 +--
 .../FineTuning/FineTuningEndpoint.cs          |  64 ++--------
 OpenAI-DotNet/Images/ImagesEndpoint.cs        |  18 +--
 OpenAI-DotNet/Models/ModelsEndpoint.cs        |  12 +-
 .../Moderations/ModerationsEndpoint.cs        |   6 +-
 OpenAI-DotNet/OpenAI-DotNet.csproj            |   2 +
 OpenAI-DotNet/Threads/ThreadsEndpoint.cs      |  91 +++++++--------
 14 files changed, 292 insertions(+), 197 deletions(-)

diff --git a/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs
index f5748552..7a6465f1 100644
--- a/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs
+++ b/OpenAI-DotNet-Tests/TestFixture_00_02_Tools.cs
@@ -2,11 +2,9 @@
 
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.Linq;
 using System.Text.Json;
 using System.Text.Json.Nodes;
-using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 using NUnit.Framework;
 using OpenAI.Images;
diff --git a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs
index 8c6d86ae..b51a78b6 100644
--- a/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs
+++ b/OpenAI-DotNet/Assistants/AssistantsEndpoint.cs
@@ -23,8 +23,8 @@ internal AssistantsEndpoint(OpenAIClient client) : base(client) { }
         /// <returns><see cref="ListResponse{Assistant}"/></returns>
         public async Task<ListResponse<AssistantResponse>> ListAssistantsAsync(ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<AssistantResponse>>(responseAsString, client);
         }
 
@@ -37,9 +37,9 @@ public async Task<ListResponse<AssistantResponse>> ListAssistantsAsync(ListQuery
         public async Task<AssistantResponse> CreateAssistantAsync(CreateAssistantRequest request = null, CancellationToken cancellationToken = default)
         {
             request ??= new CreateAssistantRequest();
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<AssistantResponse>(responseAsString, client);
         }
 
@@ -51,8 +51,8 @@ public async Task<AssistantResponse> CreateAssistantAsync(CreateAssistantRequest
         /// <returns><see cref="AssistantResponse"/>.</returns>
         public async Task<AssistantResponse> RetrieveAssistantAsync(string assistantId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<AssistantResponse>(responseAsString, client);
         }
 
@@ -65,9 +65,9 @@ public async Task<AssistantResponse> RetrieveAssistantAsync(string assistantId,
         /// <returns><see cref="AssistantResponse"/>.</returns>
         public async Task<AssistantResponse> ModifyAssistantAsync(string assistantId, CreateAssistantRequest request, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{assistantId}"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{assistantId}"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<AssistantResponse>(responseAsString, client);
         }
 
@@ -79,8 +79,8 @@ public async Task<AssistantResponse> ModifyAssistantAsync(string assistantId, Cr
         /// <returns>True, if the assistant was deleted.</returns>
         public async Task<bool> DeleteAssistantAsync(string assistantId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false;
         }
 
@@ -95,8 +95,8 @@ public async Task<bool> DeleteAssistantAsync(string assistantId, CancellationTok
         /// <returns><see cref="ListResponse{AssistantFile}"/>.</returns>
         public async Task<ListResponse<AssistantFileResponse>> ListFilesAsync(string assistantId, ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files", query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files", query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<AssistantFileResponse>>(responseAsString, client);
         }
 
@@ -117,9 +117,9 @@ public async Task<AssistantFileResponse> AttachFileAsync(string assistantId, Fil
                 throw new InvalidOperationException($"{nameof(file)}.{nameof(file.Purpose)} must be 'assistants'!");
             }
 
-            var jsonContent = JsonSerializer.Serialize(new { file_id = file.Id }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{assistantId}/files"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(new { file_id = file.Id }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{assistantId}/files"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<AssistantFileResponse>(responseAsString, client);
         }
 
@@ -132,8 +132,8 @@ public async Task<AssistantFileResponse> AttachFileAsync(string assistantId, Fil
         /// <returns><see cref="AssistantFileResponse"/>.</returns>
         public async Task<AssistantFileResponse> RetrieveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<AssistantFileResponse>(responseAsString, client);
         }
 
@@ -151,8 +151,8 @@ public async Task<AssistantFileResponse> RetrieveFileAsync(string assistantId, s
         /// <returns>True, if file was removed.</returns>
         public async Task<bool> RemoveFileAsync(string assistantId, string fileId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.DeleteAsync(GetUrl($"/{assistantId}/files/{fileId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false;
         }
 
diff --git a/OpenAI-DotNet/Audio/AudioEndpoint.cs b/OpenAI-DotNet/Audio/AudioEndpoint.cs
index c1d40bcc..7bb9997a 100644
--- a/OpenAI-DotNet/Audio/AudioEndpoint.cs
+++ b/OpenAI-DotNet/Audio/AudioEndpoint.cs
@@ -43,9 +43,9 @@ public AudioEndpoint(OpenAIClient client) : base(client) { }
         /// <returns><see cref="ReadOnlyMemory{T}"/></returns>
         public async Task<ReadOnlyMemory<byte>> CreateSpeechAsync(SpeechRequest request, Func<ReadOnlyMemory<byte>, Task> chunkCallback = null, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl("/speech"), jsonContent, cancellationToken).ConfigureAwait(false);
-            await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl("/speech"), jsonContent, cancellationToken).ConfigureAwait(false);
+            await response.CheckResponseAsync(false, jsonContent, null, cancellationToken).ConfigureAwait(false);
             await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             await using var memoryStream = new MemoryStream();
             int bytesRead;
@@ -71,6 +71,7 @@ public async Task<ReadOnlyMemory<byte>> CreateSpeechAsync(SpeechRequest request,
                 totalBytesRead += bytesRead;
             }
 
+            await response.CheckResponseAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return new ReadOnlyMemory<byte>(memoryStream.GetBuffer(), 0, totalBytesRead);
         }
 
@@ -108,8 +109,8 @@ public async Task<string> CreateTranscriptionAsync(AudioTranscriptionRequest req
 
             request.Dispose();
 
-            var response = await client.Client.PostAsync(GetUrl("/transcriptions"), content, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.PostAsync(GetUrl("/transcriptions"), content, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, null, cancellationToken).ConfigureAwait(false);
 
             return responseFormat == AudioResponseFormat.Json
                 ? JsonSerializer.Deserialize<AudioResponse>(responseAsString)?.Text
@@ -145,8 +146,8 @@ public async Task<string> CreateTranslationAsync(AudioTranslationRequest request
 
             request.Dispose();
 
-            var response = await client.Client.PostAsync(GetUrl("/translations"), content, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.PostAsync(GetUrl("/translations"), content, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, null, cancellationToken).ConfigureAwait(false);
 
             return responseFormat == AudioResponseFormat.Json
                 ? JsonSerializer.Deserialize<AudioResponse>(responseAsString)?.Text
diff --git a/OpenAI-DotNet/Chat/ChatEndpoint.cs b/OpenAI-DotNet/Chat/ChatEndpoint.cs
index 54c5e884..8a6d2d5f 100644
--- a/OpenAI-DotNet/Chat/ChatEndpoint.cs
+++ b/OpenAI-DotNet/Chat/ChatEndpoint.cs
@@ -6,7 +6,9 @@
 using System.IO;
 using System.Net.Http;
 using System.Runtime.CompilerServices;
+using System.Text;
 using System.Text.Json;
+using System.Text.Json.Nodes;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -32,9 +34,9 @@ public ChatEndpoint(OpenAIClient client) : base(client) { }
         /// <returns><see cref="ChatResponse"/>.</returns>
         public async Task<ChatResponse> GetCompletionAsync(ChatRequest chatRequest, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl("/completions"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl("/completions"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ChatResponse>(responseAsString, client);
         }
 
@@ -48,25 +50,48 @@ public async Task<ChatResponse> GetCompletionAsync(ChatRequest chatRequest, Canc
         public async Task<ChatResponse> StreamCompletionAsync(ChatRequest chatRequest, Action<ChatResponse> resultHandler, CancellationToken cancellationToken = default)
         {
             chatRequest.Stream = true;
-            var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
+            using var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
             using var request = new HttpRequestMessage(HttpMethod.Post, GetUrl("/completions"));
             request.Content = jsonContent;
-            var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-            await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            await response.CheckResponseAsync(false, jsonContent, null, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             using var reader = new StreamReader(stream);
             ChatResponse chatResponse = null;
+            using var responseStream = EnableDebug ? new MemoryStream() : null;
+
+            if (responseStream != null)
+            {
+                await responseStream.WriteAsync("["u8.ToArray(), cancellationToken);
+            }
 
             while (await reader.ReadLineAsync().ConfigureAwait(false) is { } streamData)
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                if (!streamData.TryGetEventStreamData(out var eventData)) { continue; }
+                if (!streamData.TryGetEventStreamData(out var eventData))
+                {
+                    // if response stream is not null, remove last comma
+                    responseStream?.SetLength(responseStream.Length - 1);
+                    continue;
+                }
+
                 if (string.IsNullOrWhiteSpace(eventData)) { continue; }
 
-                if (EnableDebug)
+                if (responseStream != null)
                 {
-                    Console.WriteLine(eventData);
+                    string data;
+
+                    try
+                    {
+                        data = JsonNode.Parse(eventData)?.ToJsonString(OpenAIClient.JsonSerializationOptions);
+                    }
+                    catch
+                    {
+                        data = $"{{{eventData}}}";
+                    }
+
+                    await responseStream.WriteAsync(Encoding.UTF8.GetBytes($"{data},"), cancellationToken);
                 }
 
                 var partialResponse = response.Deserialize<ChatResponse>(eventData, client);
@@ -83,7 +108,12 @@ public async Task<ChatResponse> StreamCompletionAsync(ChatRequest chatRequest, A
                 resultHandler?.Invoke(partialResponse);
             }
 
-            response.EnsureSuccessStatusCode();
+            if (responseStream != null)
+            {
+                await responseStream.WriteAsync("]"u8.ToArray(), cancellationToken);
+            }
+
+            await response.CheckResponseAsync(EnableDebug, jsonContent, responseStream, cancellationToken).ConfigureAwait(false);
 
             if (chatResponse == null) { return null; }
 
@@ -103,25 +133,48 @@ public async Task<ChatResponse> StreamCompletionAsync(ChatRequest chatRequest, A
         public async IAsyncEnumerable<ChatResponse> StreamCompletionEnumerableAsync(ChatRequest chatRequest, [EnumeratorCancellation] CancellationToken cancellationToken = default)
         {
             chatRequest.Stream = true;
-            var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
+            using var jsonContent = JsonSerializer.Serialize(chatRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
             using var request = new HttpRequestMessage(HttpMethod.Post, GetUrl("/completions"));
             request.Content = jsonContent;
-            var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-            await response.CheckResponseAsync(cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            await response.CheckResponseAsync(false, jsonContent, null, cancellationToken).ConfigureAwait(false);
             await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
             using var reader = new StreamReader(stream);
             ChatResponse chatResponse = null;
+            using var responseStream = EnableDebug ? new MemoryStream() : null;
+
+            if (responseStream != null)
+            {
+                await responseStream.WriteAsync("["u8.ToArray(), cancellationToken);
+            }
 
             while (await reader.ReadLineAsync() is { } streamData)
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                if (!streamData.TryGetEventStreamData(out var eventData)) { continue; }
+                if (!streamData.TryGetEventStreamData(out var eventData))
+                {
+                    // if response stream is not null, remove last comma
+                    responseStream?.SetLength(responseStream.Length - 1);
+                    continue;
+                }
+
                 if (string.IsNullOrWhiteSpace(eventData)) { continue; }
 
-                if (EnableDebug)
+                if (responseStream != null)
                 {
-                    Console.WriteLine(eventData);
+                    string data;
+
+                    try
+                    {
+                        data = JsonNode.Parse(eventData)?.ToJsonString(OpenAIClient.JsonSerializationOptions);
+                    }
+                    catch
+                    {
+                        data = $"{{{eventData}}}";
+                    }
+
+                    await responseStream.WriteAsync(Encoding.UTF8.GetBytes($"{data},"), cancellationToken);
                 }
 
                 var partialResponse = response.Deserialize<ChatResponse>(eventData, client);
@@ -138,7 +191,12 @@ public async IAsyncEnumerable<ChatResponse> StreamCompletionEnumerableAsync(Chat
                 yield return partialResponse;
             }
 
-            response.EnsureSuccessStatusCode();
+            if (responseStream != null)
+            {
+                await responseStream.WriteAsync("]"u8.ToArray(), cancellationToken);
+            }
+
+            await response.CheckResponseAsync(EnableDebug, jsonContent, responseStream, cancellationToken).ConfigureAwait(false);
 
             if (chatResponse == null) { yield break; }
 
diff --git a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
index c722124f..6d9cdaec 100644
--- a/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
+++ b/OpenAI-DotNet/Embeddings/EmbeddingsEndpoint.cs
@@ -76,9 +76,9 @@ public async Task<EmbeddingsResponse> CreateEmbeddingAsync(IEnumerable<string> i
         /// <returns><see cref="EmbeddingsResponse"/></returns>
         public async Task<EmbeddingsResponse> CreateEmbeddingAsync(EmbeddingsRequest request, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<EmbeddingsResponse>(responseAsString, client);
         }
     }
diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
index 53166452..fcc5e822 100644
--- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs
+++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
@@ -1,12 +1,16 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
 using System;
+using System.Collections.Generic;
 using System.Globalization;
+using System.IO;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Runtime.CompilerServices;
+using System.Text;
 using System.Text.Json;
+using System.Text.Json.Nodes;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -102,29 +106,110 @@ internal static void SetResponseData(this BaseResponse response, HttpResponseHea
             }
         }
 
-        internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse = false, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null)
+        internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse, HttpContent requestContent = null, MemoryStream responseStream = null, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null)
         {
             var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
 
-            if (!response.IsSuccessStatusCode)
+            if (debugResponse || !response.IsSuccessStatusCode)
             {
-                throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode);
-            }
-
-            if (debugResponse)
-            {
-                Console.WriteLine(responseAsString);
+                if (response.RequestMessage != null)
+                {
+                    var debugMessage = new StringBuilder();
+
+                    if (!string.IsNullOrWhiteSpace(methodName))
+                    {
+                        debugMessage.Append($"{methodName} -> ");
+                    }
+
+                    debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n");
+
+                    var debugMessageObject = new Dictionary<string, Dictionary<string, object>>
+                    {
+                        ["Request"] = new()
+                        {
+                            ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
+                        }
+                    };
+
+                    if (requestContent != null)
+                    {
+                        var requestAsString = await requestContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+
+                        if (!string.IsNullOrWhiteSpace(requestAsString))
+                        {
+                            try
+                            {
+                                debugMessageObject["Request"]["Body"] = JsonNode.Parse(requestAsString);
+                            }
+                            catch
+                            {
+                                debugMessageObject["Request"]["Body"] = requestAsString;
+                            }
+                        }
+                    }
+
+                    debugMessageObject["Response"] = new()
+                    {
+                        ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
+                    };
+
+                    if (responseStream != null || string.IsNullOrWhiteSpace(responseAsString))
+                    {
+                        debugMessageObject["Response"]["Body"] = new Dictionary<string, object>();
+                    }
+
+                    if (responseStream != null)
+                    {
+                        var body = Encoding.UTF8.GetString(responseStream.ToArray());
+
+                        try
+                        {
+                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = JsonNode.Parse(body);
+                        }
+                        catch
+                        {
+                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = body;
+                        }
+                    }
+
+                    if (!string.IsNullOrWhiteSpace(responseAsString))
+                    {
+                        try
+                        {
+                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString);
+                        }
+                        catch
+                        {
+                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = responseAsString;
+                        }
+                    }
+
+                    debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true }));
+
+                    if (!response.IsSuccessStatusCode)
+                    {
+                        throw new HttpRequestException(debugMessage.ToString());
+                    }
+
+                    if (debugResponse)
+                    {
+                        Console.WriteLine(debugMessage.ToString());
+                    }
+                }
+                else
+                {
+                    throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode);
+                }
             }
 
             return responseAsString;
         }
 
-        internal static async Task CheckResponseAsync(this HttpResponseMessage response, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null)
+        internal static async Task CheckResponseAsync(this HttpResponseMessage response, bool debug, StringContent requestContent = null, MemoryStream responseStream = null, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null)
         {
-            if (!response.IsSuccessStatusCode)
+            if (!response.IsSuccessStatusCode || debug)
             {
-                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);
+                await response.ReadAsStringAsync(debug, requestContent, responseStream, cancellationToken, methodName).ConfigureAwait(false);
             }
         }
 
diff --git a/OpenAI-DotNet/Extensions/StringExtensions.cs b/OpenAI-DotNet/Extensions/StringExtensions.cs
index 4308767a..fc8b289e 100644
--- a/OpenAI-DotNet/Extensions/StringExtensions.cs
+++ b/OpenAI-DotNet/Extensions/StringExtensions.cs
@@ -1,9 +1,7 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
-using System;
 using System.Linq;
 using System.Net.Http;
-using System.Text;
 
 namespace OpenAI.Extensions
 {
@@ -30,16 +28,10 @@ public static bool TryGetEventStreamData(this string streamData, out string even
             return eventData != doneTag;
         }
 
-        public static StringContent ToJsonStringContent(this string json, bool debug)
+        public static StringContent ToJsonStringContent(this string json)
         {
             const string jsonContent = "application/json";
-
-            if (debug)
-            {
-                Console.WriteLine(json);
-            }
-
-            return new StringContent(json, Encoding.UTF8, jsonContent);
+            return new StringContent(json, null, jsonContent);
         }
 
         public static string ToSnakeCase(string @string)
diff --git a/OpenAI-DotNet/Files/FilesEndpoint.cs b/OpenAI-DotNet/Files/FilesEndpoint.cs
index ae09f5f6..4fda6ee5 100644
--- a/OpenAI-DotNet/Files/FilesEndpoint.cs
+++ b/OpenAI-DotNet/Files/FilesEndpoint.cs
@@ -46,8 +46,8 @@ public async Task<IReadOnlyList<FileResponse>> ListFilesAsync(string purpose = n
                 query = new Dictionary<string, string> { { nameof(purpose), purpose } };
             }
 
-            var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false);
-            var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl(queryParameters: query), cancellationToken).ConfigureAwait(false);
+            var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<FilesList>(resultAsString, OpenAIClient.JsonSerializationOptions)?.Files;
         }
 
@@ -85,8 +85,8 @@ public async Task<FileResponse> UploadFileAsync(FileUploadRequest request, Cance
             content.Add(new StringContent(request.Purpose), "purpose");
             content.Add(new ByteArrayContent(fileData.ToArray()), "file", request.FileName);
             request.Dispose();
-            var response = await client.Client.PostAsync(GetUrl(), content, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.PostAsync(GetUrl(), content, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<FileResponse>(responseAsString, OpenAIClient.JsonSerializationOptions);
         }
 
@@ -102,7 +102,7 @@ public async Task<bool> DeleteFileAsync(string fileId, CancellationToken cancell
 
             async Task<bool> InternalDeleteFileAsync(int attempt)
             {
-                var response = await client.Client.DeleteAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false);
+                using var response = await client.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);
 
@@ -120,7 +120,7 @@ async Task<bool> InternalDeleteFileAsync(int attempt)
                     }
                 }
 
-                await response.CheckResponseAsync(cancellationToken);
+                await response.CheckResponseAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
                 return JsonSerializer.Deserialize<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false;
             }
         }
@@ -133,8 +133,8 @@ async Task<bool> InternalDeleteFileAsync(int attempt)
         /// <returns><see cref="FileResponse"/></returns>
         public async Task<FileResponse> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{fileId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<FileResponse>(responseAsString, OpenAIClient.JsonSerializationOptions);
         }
 
@@ -191,7 +191,7 @@ public async Task<string> DownloadFileAsync(FileResponse fileData, string direct
             await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
             return filePath;
         }
-        
+
         /// <summary>
         /// Gets the specified file as stream
         /// </summary>
diff --git a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs
index 5b92fc42..ce646b46 100644
--- a/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs
+++ b/OpenAI-DotNet/FineTuning/FineTuningEndpoint.cs
@@ -1,8 +1,6 @@
 // Licensed under the MIT License. See LICENSE in the project root for license information.
 
 using OpenAI.Extensions;
-using System;
-using System.Collections.Generic;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
@@ -32,32 +30,12 @@ public FineTuningEndpoint(OpenAIClient client) : base(client) { }
         /// <returns><see cref="FineTuneJobResponse"/>.</returns>
         public async Task<FineTuneJobResponse> CreateJobAsync(CreateFineTuneJobRequest jobRequest, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(jobRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl("/jobs"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(jobRequest, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl("/jobs"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<FineTuneJobResponse>(responseAsString, client);
         }
 
-        [Obsolete("Use new overload")]
-        public async Task<FineTuneJobList> ListJobsAsync(int? limit, string after, CancellationToken cancellationToken)
-        {
-            var parameters = new Dictionary<string, string>();
-
-            if (limit.HasValue)
-            {
-                parameters.Add(nameof(limit), limit.ToString());
-            }
-
-            if (!string.IsNullOrWhiteSpace(after))
-            {
-                parameters.Add(nameof(after), after);
-            }
-
-            var response = await client.Client.GetAsync(GetUrl("/jobs", parameters), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
-            return JsonSerializer.Deserialize<FineTuneJobList>(responseAsString, OpenAIClient.JsonSerializationOptions);
-        }
-
         /// <summary>
         /// List your organization's fine-tuning jobs.
         /// </summary>
@@ -66,8 +44,8 @@ public async Task<FineTuneJobList> ListJobsAsync(int? limit, string after, Cance
         /// <returns>List of <see cref="FineTuneJobResponse"/>s.</returns>
         public async Task<ListResponse<FineTuneJobResponse>> ListJobsAsync(ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl("/jobs", query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl("/jobs", query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<FineTuneJobResponse>>(responseAsString, client);
         }
 
@@ -79,8 +57,8 @@ public async Task<ListResponse<FineTuneJobResponse>> ListJobsAsync(ListQuery que
         /// <returns><see cref="FineTuneJobResponse"/>.</returns>
         public async Task<FineTuneJobResponse> GetJobInfoAsync(string jobId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             var job = response.Deserialize<FineTuneJobResponse>(responseAsString, client);
             job.Events = (await ListJobEventsAsync(job, query: null, cancellationToken: cancellationToken).ConfigureAwait(false))?.Items;
             return job;
@@ -94,32 +72,12 @@ public async Task<FineTuneJobResponse> GetJobInfoAsync(string jobId, Cancellatio
         /// <returns><see cref="FineTuneJobResponse"/>.</returns>
         public async Task<bool> CancelJobAsync(string jobId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.PostAsync(GetUrl($"/jobs/{jobId}/cancel"), null!, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.PostAsync(GetUrl($"/jobs/{jobId}/cancel"), null!, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             var result = JsonSerializer.Deserialize<FineTuneJobResponse>(responseAsString, OpenAIClient.JsonSerializationOptions);
             return result.Status == JobStatus.Cancelled;
         }
 
-        [Obsolete("use new overload")]
-        public async Task<EventList> ListJobEventsAsync(string jobId, int? limit, string after, CancellationToken cancellationToken)
-        {
-            var parameters = new Dictionary<string, string>();
-
-            if (limit.HasValue)
-            {
-                parameters.Add(nameof(limit), limit.ToString());
-            }
-
-            if (!string.IsNullOrWhiteSpace(after))
-            {
-                parameters.Add(nameof(after), after);
-            }
-
-            var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}/events", parameters), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
-            return JsonSerializer.Deserialize<EventList>(responseAsString, OpenAIClient.JsonSerializationOptions);
-        }
-
         /// <summary>
         /// Get fine-grained status updates for a fine-tune job.
         /// </summary>
@@ -129,8 +87,8 @@ public async Task<EventList> ListJobEventsAsync(string jobId, int? limit, string
         /// <returns>List of events for <see cref="FineTuneJobResponse"/>.</returns>
         public async Task<ListResponse<EventResponse>> ListJobEventsAsync(string jobId, ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}/events", query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/jobs/{jobId}/events", query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<EventResponse>>(responseAsString, client);
         }
     }
diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs
index 74ff8198..0d648458 100644
--- a/OpenAI-DotNet/Images/ImagesEndpoint.cs
+++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs
@@ -30,9 +30,9 @@ internal ImagesEndpoint(OpenAIClient client) : base(client) { }
         /// <returns>A list of generated texture urls to download.</returns>
         public async Task<IReadOnlyList<ImageResult>> GenerateImageAsync(ImageGenerationRequest request, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl("/generations"), jsonContent, cancellationToken).ConfigureAwait(false);
-            return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl("/generations"), jsonContent, cancellationToken).ConfigureAwait(false);
+            return await DeserializeResponseAsync(response, jsonContent, cancellationToken).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -66,8 +66,8 @@ public async Task<IReadOnlyList<ImageResult>> CreateImageEditAsync(ImageEditRequ
             }
 
             request.Dispose();
-            var response = await client.Client.PostAsync(GetUrl("/edits"), content, cancellationToken).ConfigureAwait(false);
-            return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.PostAsync(GetUrl("/edits"), content, cancellationToken).ConfigureAwait(false);
+            return await DeserializeResponseAsync(response, content, cancellationToken).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -92,13 +92,13 @@ public async Task<IReadOnlyList<ImageResult>> CreateImageVariationAsync(ImageVar
             }
 
             request.Dispose();
-            var response = await client.Client.PostAsync(GetUrl("/variations"), content, cancellationToken).ConfigureAwait(false);
-            return await DeserializeResponseAsync(response, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.PostAsync(GetUrl("/variations"), content, cancellationToken).ConfigureAwait(false);
+            return await DeserializeResponseAsync(response, content, cancellationToken).ConfigureAwait(false);
         }
 
-        private async Task<IReadOnlyList<ImageResult>> DeserializeResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default)
+        private async Task<IReadOnlyList<ImageResult>> DeserializeResponseAsync(HttpResponseMessage response, HttpContent requestContent, CancellationToken cancellationToken = default)
         {
-            var resultAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            var resultAsString = await response.ReadAsStringAsync(EnableDebug, requestContent, null, cancellationToken).ConfigureAwait(false);
             var imagesResponse = response.Deserialize<ImagesResponse>(resultAsString, client);
 
             if (imagesResponse?.Results is not { Count: not 0 })
diff --git a/OpenAI-DotNet/Models/ModelsEndpoint.cs b/OpenAI-DotNet/Models/ModelsEndpoint.cs
index 12161f7c..3db14fff 100644
--- a/OpenAI-DotNet/Models/ModelsEndpoint.cs
+++ b/OpenAI-DotNet/Models/ModelsEndpoint.cs
@@ -37,8 +37,8 @@ public ModelsEndpoint(OpenAIClient client) : base(client) { }
         /// <returns>Asynchronously returns the list of all <see cref="Model"/>s</returns>
         public async Task<IReadOnlyList<Model>> GetModelsAsync(CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl(), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<ModelsList>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Models;
         }
 
@@ -50,8 +50,8 @@ public async Task<IReadOnlyList<Model>> GetModelsAsync(CancellationToken cancell
         /// <returns>Asynchronously returns the <see cref="Model"/> with all available properties</returns>
         public async Task<Model> GetModelDetailsAsync(string id, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{id}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{id}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<Model>(responseAsString, OpenAIClient.JsonSerializationOptions);
         }
 
@@ -75,8 +75,8 @@ public async Task<bool> DeleteFineTuneModelAsync(string modelId, CancellationTok
 
             try
             {
-                var response = await client.Client.DeleteAsync(GetUrl($"/{model.Id}"), cancellationToken).ConfigureAwait(false);
-                var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+                using var response = await client.Client.DeleteAsync(GetUrl($"/{model.Id}"), cancellationToken).ConfigureAwait(false);
+                var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
                 return JsonSerializer.Deserialize<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false;
             }
             catch (Exception e)
diff --git a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs
index a1edf7cc..0645c471 100644
--- a/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs
+++ b/OpenAI-DotNet/Moderations/ModerationsEndpoint.cs
@@ -50,9 +50,9 @@ public async Task<bool> GetModerationAsync(string input, string model = null, Ca
         /// <param name="cancellationToken">Optional, <see cref="CancellationToken"/>.</param>
         public async Task<ModerationsResponse> CreateModerationAsync(ModerationsRequest request, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ModerationsResponse>(responseAsString, client);
         }
 
diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj
index 0c8d259e..4534d296 100644
--- a/OpenAI-DotNet/OpenAI-DotNet.csproj
+++ b/OpenAI-DotNet/OpenAI-DotNet.csproj
@@ -39,6 +39,8 @@ Version 7.7.1
   - Added additional tool utilities:
     - Tool.ClearRegisteredTools
     - Tool.IsToolRegistered(Tool) - Tool.TryRegisterTool(Tool)
+  - Improved memory usage and performance by propertly disposing http content and response objects
+  - Updated debug output to be formatted to json for easier reading and debugging
 Version 7.7.0
 - Added Tool call and Function call Utilities and helper methods
   - Added FunctionAttribute to decorate methods to be used in function calling
diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs
index 8c66df2d..14c5ccc0 100644
--- a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs
+++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs
@@ -26,8 +26,9 @@ public ThreadsEndpoint(OpenAIClient client) : base(client) { }
         /// <returns><see cref="ThreadResponse"/>.</returns>
         public async Task<ThreadResponse> CreateThreadAsync(CreateThreadRequest request = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.PostAsync(GetUrl(), request != null ? JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug) : null, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = request != null ? JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent() : null;
+            using var response = await client.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ThreadResponse>(responseAsString, client);
         }
 
@@ -39,8 +40,8 @@ public async Task<ThreadResponse> CreateThreadAsync(CreateThreadRequest request
         /// <returns><see cref="ThreadResponse"/>.</returns>
         public async Task<ThreadResponse> RetrieveThreadAsync(string threadId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ThreadResponse>(responseAsString, client);
         }
 
@@ -59,9 +60,9 @@ public async Task<ThreadResponse> RetrieveThreadAsync(string threadId, Cancellat
         /// <returns><see cref="ThreadResponse"/>.</returns>
         public async Task<ThreadResponse> ModifyThreadAsync(string threadId, IReadOnlyDictionary<string, string> metadata, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{threadId}"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{threadId}"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ThreadResponse>(responseAsString, client);
         }
 
@@ -73,8 +74,8 @@ public async Task<ThreadResponse> ModifyThreadAsync(string threadId, IReadOnlyDi
         /// <returns>True, if was successfully deleted.</returns>
         public async Task<bool> DeleteThreadAsync(string threadId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.DeleteAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.DeleteAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return JsonSerializer.Deserialize<DeletedResponse>(responseAsString, OpenAIClient.JsonSerializationOptions)?.Deleted ?? false;
         }
 
@@ -89,9 +90,9 @@ public async Task<bool> DeleteThreadAsync(string threadId, CancellationToken can
         /// <returns><see cref="MessageResponse"/>.</returns>
         public async Task<MessageResponse> CreateMessageAsync(string threadId, CreateMessageRequest request, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<MessageResponse>(responseAsString, client);
         }
 
@@ -104,8 +105,8 @@ public async Task<MessageResponse> CreateMessageAsync(string threadId, CreateMes
         /// <returns><see cref="ListResponse{ThreadMessage}"/>.</returns>
         public async Task<ListResponse<MessageResponse>> ListMessagesAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages", query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages", query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<MessageResponse>>(responseAsString, client);
         }
 
@@ -118,8 +119,8 @@ public async Task<ListResponse<MessageResponse>> ListMessagesAsync(string thread
         /// <returns><see cref="MessageResponse"/>.</returns>
         public async Task<MessageResponse> RetrieveMessageAsync(string threadId, string messageId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<MessageResponse>(responseAsString, client);
         }
 
@@ -155,9 +156,9 @@ public async Task<MessageResponse> ModifyMessageAsync(MessageResponse message, I
         /// <returns><see cref="MessageResponse"/>.</returns>
         public async Task<MessageResponse> ModifyMessageAsync(string threadId, string messageId, IReadOnlyDictionary<string, string> metadata, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages/{messageId}"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/messages/{messageId}"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<MessageResponse>(responseAsString, client);
         }
 
@@ -175,8 +176,8 @@ public async Task<MessageResponse> ModifyMessageAsync(string threadId, string me
         /// <returns><see cref="ListResponse{ThreadMessageFile}"/>.</returns>
         public async Task<ListResponse<MessageFileResponse>> ListFilesAsync(string threadId, string messageId, ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files", query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files", query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<MessageFileResponse>>(responseAsString, client);
         }
 
@@ -190,8 +191,8 @@ public async Task<ListResponse<MessageFileResponse>> ListFilesAsync(string threa
         /// <returns><see cref="MessageFileResponse"/>.</returns>
         public async Task<MessageFileResponse> RetrieveFileAsync(string threadId, string messageId, string fileId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files/{fileId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/messages/{messageId}/files/{fileId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<MessageFileResponse>(responseAsString, client);
         }
 
@@ -208,8 +209,8 @@ public async Task<MessageFileResponse> RetrieveFileAsync(string threadId, string
         /// <returns><see cref="ListResponse{RunResponse}"/></returns>
         public async Task<ListResponse<RunResponse>> ListRunsAsync(string threadId, ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs", query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs", query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<RunResponse>>(responseAsString, client);
         }
 
@@ -228,9 +229,9 @@ public async Task<RunResponse> CreateRunAsync(string threadId, CreateRunRequest
                 request = new CreateRunRequest(assistant, request);
             }
 
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<RunResponse>(responseAsString, client);
         }
 
@@ -248,9 +249,9 @@ public async Task<RunResponse> CreateThreadAndRunAsync(CreateThreadAndRunRequest
                 request = new CreateThreadAndRunRequest(assistant, request);
             }
 
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl("/runs"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl("/runs"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<RunResponse>(responseAsString, client);
         }
 
@@ -263,8 +264,8 @@ public async Task<RunResponse> CreateThreadAndRunAsync(CreateThreadAndRunRequest
         /// <returns><see cref="RunResponse"/>.</returns>
         public async Task<RunResponse> RetrieveRunAsync(string threadId, string runId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<RunResponse>(responseAsString, client);
         }
 
@@ -283,9 +284,9 @@ public async Task<RunResponse> RetrieveRunAsync(string threadId, string runId, C
         /// <returns><see cref="RunResponse"/>.</returns>
         public async Task<RunResponse> ModifyRunAsync(string threadId, string runId, IReadOnlyDictionary<string, string> metadata, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(new { metadata }, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<RunResponse>(responseAsString, client);
         }
 
@@ -301,9 +302,9 @@ public async Task<RunResponse> ModifyRunAsync(string threadId, string runId, IRe
         /// <returns><see cref="RunResponse"/>.</returns>
         public async Task<RunResponse> SubmitToolOutputsAsync(string threadId, string runId, SubmitToolOutputsRequest request, CancellationToken cancellationToken = default)
         {
-            var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent(EnableDebug);
-            var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/submit_tool_outputs"), jsonContent, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions).ToJsonStringContent();
+            using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/submit_tool_outputs"), jsonContent, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, jsonContent, null, cancellationToken).ConfigureAwait(false);
             return response.Deserialize<RunResponse>(responseAsString, client);
         }
 
@@ -317,8 +318,8 @@ public async Task<RunResponse> SubmitToolOutputsAsync(string threadId, string ru
         /// <returns><see cref="ListResponse{RunStep}"/>.</returns>
         public async Task<ListResponse<RunStepResponse>> ListRunStepsAsync(string threadId, string runId, ListQuery query = null, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps", query), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps", query), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<ListResponse<RunStepResponse>>(responseAsString, client);
         }
 
@@ -332,8 +333,8 @@ public async Task<ListResponse<RunStepResponse>> ListRunStepsAsync(string thread
         /// <returns><see cref="RunStepResponse"/>.</returns>
         public async Task<RunStepResponse> RetrieveRunStepAsync(string threadId, string runId, string stepId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps/{stepId}"), cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.GetAsync(GetUrl($"/{threadId}/runs/{runId}/steps/{stepId}"), cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<RunStepResponse>(responseAsString, client);
         }
 
@@ -346,8 +347,8 @@ public async Task<RunStepResponse> RetrieveRunStepAsync(string threadId, string
         /// <returns><see cref="RunResponse"/>.</returns>
         public async Task<RunResponse> CancelRunAsync(string threadId, string runId, CancellationToken cancellationToken = default)
         {
-            var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/cancel"), content: null, cancellationToken).ConfigureAwait(false);
-            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
+            using var response = await client.Client.PostAsync(GetUrl($"/{threadId}/runs/{runId}/cancel"), content: null, cancellationToken).ConfigureAwait(false);
+            var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken: cancellationToken).ConfigureAwait(false);
             return response.Deserialize<RunResponse>(responseAsString, client);
         }
 

From 02609ac47e5fe8575cb78db1c04dec5fc33c3489 Mon Sep 17 00:00:00 2001
From: Stephen Hodgson <hodgson.designs@gmail.com>
Date: Sun, 25 Feb 2024 15:09:48 -0500
Subject: [PATCH 4/7] fix logical error

---
 OpenAI-DotNet/Extensions/ResponseExtensions.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
index fcc5e822..b0ab2bcd 100644
--- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs
+++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
@@ -153,7 +153,7 @@ internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage re
                         ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
                     };
 
-                    if (responseStream != null || string.IsNullOrWhiteSpace(responseAsString))
+                    if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString))
                     {
                         debugMessageObject["Response"]["Body"] = new Dictionary<string, object>();
                     }

From 03331d6f133d5e58f8df9d565aa902e4245458df Mon Sep 17 00:00:00 2001
From: Stephen Hodgson <hodgson.designs@gmail.com>
Date: Sun, 25 Feb 2024 15:12:59 -0500
Subject: [PATCH 5/7] revert

---
 OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs
index 2b03f857..c47d3f6a 100644
--- a/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs
+++ b/OpenAI-DotNet-Tests/TestFixture_00_01_Authentication.cs
@@ -187,8 +187,6 @@ public void TearDown()
             }
 
             Assert.IsFalse(File.Exists(".openai"));
-            
-            OpenAIAuthentication.Default = null;
         }
     }
 }

From ac4f93f8454fbfe86d226ea96b18279ba720966b Mon Sep 17 00:00:00 2001
From: Stephen Hodgson <hodgson.designs@gmail.com>
Date: Sun, 25 Feb 2024 15:17:41 -0500
Subject: [PATCH 6/7] fix http exception

---
 .../Extensions/ResponseExtensions.cs          | 119 +++++++++---------
 1 file changed, 57 insertions(+), 62 deletions(-)

diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
index b0ab2bcd..aae116d8 100644
--- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs
+++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
@@ -110,96 +110,91 @@ internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage re
         {
             var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
 
-            if (debugResponse || !response.IsSuccessStatusCode)
+            if (debugResponse && response.RequestMessage != null)
             {
-                if (response.RequestMessage != null)
+                var debugMessage = new StringBuilder();
+
+                if (!string.IsNullOrWhiteSpace(methodName))
                 {
-                    var debugMessage = new StringBuilder();
+                    debugMessage.Append($"{methodName} -> ");
+                }
+
+                debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n");
 
-                    if (!string.IsNullOrWhiteSpace(methodName))
+                var debugMessageObject = new Dictionary<string, Dictionary<string, object>>
+                {
+                    ["Request"] = new()
                     {
-                        debugMessage.Append($"{methodName} -> ");
+                        ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
                     }
+                };
 
-                    debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n");
+                if (requestContent != null)
+                {
+                    var requestAsString = await requestContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
 
-                    var debugMessageObject = new Dictionary<string, Dictionary<string, object>>
+                    if (!string.IsNullOrWhiteSpace(requestAsString))
                     {
-                        ["Request"] = new()
+                        try
                         {
-                            ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
+                            debugMessageObject["Request"]["Body"] = JsonNode.Parse(requestAsString);
                         }
-                    };
-
-                    if (requestContent != null)
-                    {
-                        var requestAsString = await requestContent.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
-
-                        if (!string.IsNullOrWhiteSpace(requestAsString))
+                        catch
                         {
-                            try
-                            {
-                                debugMessageObject["Request"]["Body"] = JsonNode.Parse(requestAsString);
-                            }
-                            catch
-                            {
-                                debugMessageObject["Request"]["Body"] = requestAsString;
-                            }
+                            debugMessageObject["Request"]["Body"] = requestAsString;
                         }
                     }
+                }
 
-                    debugMessageObject["Response"] = new()
-                    {
-                        ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
-                    };
+                debugMessageObject["Response"] = new()
+                {
+                    ["Headers"] = response.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
+                };
 
-                    if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString))
-                    {
-                        debugMessageObject["Response"]["Body"] = new Dictionary<string, object>();
-                    }
+                if (responseStream != null || !string.IsNullOrWhiteSpace(responseAsString))
+                {
+                    debugMessageObject["Response"]["Body"] = new Dictionary<string, object>();
+                }
 
-                    if (responseStream != null)
-                    {
-                        var body = Encoding.UTF8.GetString(responseStream.ToArray());
+                if (responseStream != null)
+                {
+                    var body = Encoding.UTF8.GetString(responseStream.ToArray());
 
-                        try
-                        {
-                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = JsonNode.Parse(body);
-                        }
-                        catch
-                        {
-                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = body;
-                        }
+                    try
+                    {
+                        ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = JsonNode.Parse(body);
                     }
-
-                    if (!string.IsNullOrWhiteSpace(responseAsString))
+                    catch
                     {
-                        try
-                        {
-                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString);
-                        }
-                        catch
-                        {
-                            ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = responseAsString;
-                        }
+                        ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Stream"] = body;
                     }
+                }
 
-                    debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true }));
-
-                    if (!response.IsSuccessStatusCode)
+                if (!string.IsNullOrWhiteSpace(responseAsString))
+                {
+                    try
                     {
-                        throw new HttpRequestException(debugMessage.ToString());
+                        ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = JsonNode.Parse(responseAsString);
                     }
-
-                    if (debugResponse)
+                    catch
                     {
-                        Console.WriteLine(debugMessage.ToString());
+                        ((Dictionary<string, object>)debugMessageObject["Response"]["Body"])["Content"] = responseAsString;
                     }
                 }
-                else
+
+                debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true }));
+
+                if (!response.IsSuccessStatusCode)
                 {
-                    throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode);
+                    throw new HttpRequestException(debugMessage.ToString());
                 }
+
+                Console.WriteLine(debugMessage.ToString());
+            }
+
+            if (!response.IsSuccessStatusCode)
+            {
+                throw new HttpRequestException(message: $"{methodName} Failed! HTTP status code: {response.StatusCode} | Response body: {responseAsString}", null, statusCode: response.StatusCode);
             }
 
             return responseAsString;

From e2db47d587ba407adf50fe6de1dd62eacdb8bbfa Mon Sep 17 00:00:00 2001
From: Stephen Hodgson <hodgson.designs@gmail.com>
Date: Sun, 25 Feb 2024 15:26:38 -0500
Subject: [PATCH 7/7] tweak http req ex a bit more

---
 .../Extensions/ResponseExtensions.cs          | 23 ++++++++-----------
 1 file changed, 9 insertions(+), 14 deletions(-)

diff --git a/OpenAI-DotNet/Extensions/ResponseExtensions.cs b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
index aae116d8..a32ece1e 100644
--- a/OpenAI-DotNet/Extensions/ResponseExtensions.cs
+++ b/OpenAI-DotNet/Extensions/ResponseExtensions.cs
@@ -109,25 +109,26 @@ internal static void SetResponseData(this BaseResponse response, HttpResponseHea
         internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage response, bool debugResponse, HttpContent requestContent = null, MemoryStream responseStream = null, CancellationToken cancellationToken = default, [CallerMemberName] string methodName = null)
         {
             var responseAsString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+            var debugMessage = new StringBuilder();
 
-            if (debugResponse && response.RequestMessage != null)
+            if (!response.IsSuccessStatusCode || debugResponse)
             {
-                var debugMessage = new StringBuilder();
-
                 if (!string.IsNullOrWhiteSpace(methodName))
                 {
                     debugMessage.Append($"{methodName} -> ");
                 }
 
-                debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n");
+                var debugMessageObject = new Dictionary<string, Dictionary<string, object>>();
 
-                var debugMessageObject = new Dictionary<string, Dictionary<string, object>>
+                if (response.RequestMessage != null)
                 {
-                    ["Request"] = new()
+                    debugMessage.Append($"[{response.RequestMessage.Method}:{(int)response.StatusCode}] {response.RequestMessage.RequestUri}\n");
+
+                    debugMessageObject["Request"] = new()
                     {
                         ["Headers"] = response.RequestMessage.Headers.ToDictionary(pair => pair.Key, pair => pair.Value),
-                    }
-                };
+                    };
+                }
 
                 if (requestContent != null)
                 {
@@ -183,12 +184,6 @@ internal static async Task<string> ReadAsStringAsync(this HttpResponseMessage re
                 }
 
                 debugMessage.Append(JsonSerializer.Serialize(debugMessageObject, new JsonSerializerOptions { WriteIndented = true }));
-
-                if (!response.IsSuccessStatusCode)
-                {
-                    throw new HttpRequestException(debugMessage.ToString());
-                }
-
                 Console.WriteLine(debugMessage.ToString());
             }