diff --git a/OpenAI-DotNet-Tests/TestFixture_02_Assistants.cs b/OpenAI-DotNet-Tests/TestFixture_02_Assistants.cs index 688c1067..8dc6b869 100644 --- a/OpenAI-DotNet-Tests/TestFixture_02_Assistants.cs +++ b/OpenAI-DotNet-Tests/TestFixture_02_Assistants.cs @@ -48,7 +48,7 @@ public async Task Test_01_Assistants() ["int"] = "1", ["test"] = Guid.NewGuid().ToString() }, - tools: new[] { Tool.FileSearch }); + tools: new[] { new Tool(new FileSearchOptions(15, new RankingOptions("auto", 0.5f))) }); var assistant = await OpenAIClient.AssistantsEndpoint.CreateAssistantAsync(request); Assert.IsNotNull(assistant); diff --git a/OpenAI-DotNet/Assistants/AssistantResponse.cs b/OpenAI-DotNet/Assistants/AssistantResponse.cs index de724fa7..eb01da86 100644 --- a/OpenAI-DotNet/Assistants/AssistantResponse.cs +++ b/OpenAI-DotNet/Assistants/AssistantResponse.cs @@ -1,5 +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.Serialization; @@ -136,6 +137,7 @@ public sealed class AssistantResponse : BaseResponse /// [JsonInclude] [JsonPropertyName("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] public ResponseFormatObject ResponseFormatObject { get; private set; } [JsonIgnore] diff --git a/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs index 6a2bb7f0..d6441a78 100644 --- a/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs +++ b/OpenAI-DotNet/Assistants/CreateAssistantRequest.cs @@ -1,5 +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.Linq; @@ -275,6 +276,7 @@ public CreateAssistantRequest( /// which indicates the generation exceeded max_tokens or the conversation exceeded the max context length. /// [JsonPropertyName("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ResponseFormatObject ResponseFormatObject { get; internal set; } diff --git a/OpenAI-DotNet/Audio/AudioEndpoint.cs b/OpenAI-DotNet/Audio/AudioEndpoint.cs index be398760..ad198fbb 100644 --- a/OpenAI-DotNet/Audio/AudioEndpoint.cs +++ b/OpenAI-DotNet/Audio/AudioEndpoint.cs @@ -100,41 +100,47 @@ public async Task CreateTranscriptionJsonAsync(AudioTranscription private async Task<(HttpResponseMessage, string)> Internal_CreateTranscriptionAsync(AudioTranscriptionRequest request, CancellationToken cancellationToken = default) { - using var content = new MultipartFormDataContent(); - using var audioData = new MemoryStream(); - await request.Audio.CopyToAsync(audioData, cancellationToken).ConfigureAwait(false); - content.Add(new ByteArrayContent(audioData.ToArray()), "file", request.AudioName); - content.Add(new StringContent(request.Model), "model"); + using var payload = new MultipartFormDataContent(); - if (!string.IsNullOrWhiteSpace(request.Language)) + try { - content.Add(new StringContent(request.Language), "language"); - } + using var audioData = new MemoryStream(); + await request.Audio.CopyToAsync(audioData, cancellationToken).ConfigureAwait(false); + payload.Add(new ByteArrayContent(audioData.ToArray()), "file", request.AudioName); + payload.Add(new StringContent(request.Model), "model"); - if (!string.IsNullOrWhiteSpace(request.Prompt)) - { - content.Add(new StringContent(request.Prompt), "prompt"); - } + if (!string.IsNullOrWhiteSpace(request.Language)) + { + payload.Add(new StringContent(request.Language), "language"); + } + + if (!string.IsNullOrWhiteSpace(request.Prompt)) + { + payload.Add(new StringContent(request.Prompt), "prompt"); + } - content.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); + payload.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); - if (request.Temperature.HasValue) - { - content.Add(new StringContent(request.Temperature.Value.ToString(CultureInfo.InvariantCulture)), "temperature"); - } + if (request.Temperature.HasValue) + { + payload.Add(new StringContent(request.Temperature.Value.ToString(CultureInfo.InvariantCulture)), "temperature"); + } - switch (request.TimestampGranularities) + switch (request.TimestampGranularities) + { + case TimestampGranularity.Segment: + case TimestampGranularity.Word: + payload.Add(new StringContent(request.TimestampGranularities.ToString().ToLower()), "timestamp_granularities[]"); + break; + } + } + finally { - case TimestampGranularity.Segment: - case TimestampGranularity.Word: - content.Add(new StringContent(request.TimestampGranularities.ToString().ToLower()), "timestamp_granularities[]"); - break; + request.Dispose(); } - request.Dispose(); - - using var response = await client.Client.PostAsync(GetUrl("/transcriptions"), content, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/transcriptions"), payload, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, payload, cancellationToken).ConfigureAwait(false); return (response, responseAsString); } @@ -172,28 +178,34 @@ public async Task CreateTranslationJsonAsync(AudioTranslationRequ private async Task<(HttpResponseMessage, string)> Internal_CreateTranslationAsync(AudioTranslationRequest request, CancellationToken cancellationToken = default) { - using var content = new MultipartFormDataContent(); - using var audioData = new MemoryStream(); - await request.Audio.CopyToAsync(audioData, cancellationToken).ConfigureAwait(false); - content.Add(new ByteArrayContent(audioData.ToArray()), "file", request.AudioName); - content.Add(new StringContent(request.Model), "model"); + using var payload = new MultipartFormDataContent(); - if (!string.IsNullOrWhiteSpace(request.Prompt)) + try { - content.Add(new StringContent(request.Prompt), "prompt"); - } + using var audioData = new MemoryStream(); + await request.Audio.CopyToAsync(audioData, cancellationToken).ConfigureAwait(false); + payload.Add(new ByteArrayContent(audioData.ToArray()), "file", request.AudioName); + payload.Add(new StringContent(request.Model), "model"); + + if (!string.IsNullOrWhiteSpace(request.Prompt)) + { + payload.Add(new StringContent(request.Prompt), "prompt"); + } - content.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); + payload.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); - if (request.Temperature.HasValue) + if (request.Temperature.HasValue) + { + payload.Add(new StringContent(request.Temperature.Value.ToString(CultureInfo.InvariantCulture)), "temperature"); + } + } + finally { - content.Add(new StringContent(request.Temperature.Value.ToString(CultureInfo.InvariantCulture)), "temperature"); + request.Dispose(); } - request.Dispose(); - - using var response = await client.Client.PostAsync(GetUrl("/translations"), content, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/translations"), payload, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, payload, cancellationToken).ConfigureAwait(false); return (response, responseAsString); } } diff --git a/OpenAI-DotNet/Chat/ChatRequest.cs b/OpenAI-DotNet/Chat/ChatRequest.cs index 2eaacb9a..92f165df 100644 --- a/OpenAI-DotNet/Chat/ChatRequest.cs +++ b/OpenAI-DotNet/Chat/ChatRequest.cs @@ -1,5 +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.Linq; @@ -281,6 +282,7 @@ public ChatRequest( public ChatResponseFormat ResponseFormat => ResponseFormatObject ?? ChatResponseFormat.Auto; [JsonPropertyName("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ResponseFormatObject ResponseFormatObject { get; internal set; } diff --git a/OpenAI-DotNet/Common/BaseResponse.cs b/OpenAI-DotNet/Common/BaseResponse.cs index 243dda07..2de45d4d 100644 --- a/OpenAI-DotNet/Common/BaseResponse.cs +++ b/OpenAI-DotNet/Common/BaseResponse.cs @@ -3,6 +3,7 @@ using OpenAI.Extensions; using System; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace OpenAI { @@ -27,7 +28,8 @@ public abstract class BaseResponse public string Organization { get; internal set; } /// - /// The request id of this API call, as reported in the response headers. This may be useful for troubleshooting or when contacting OpenAI support in reference to a specific request. + /// The request id of this API call, as reported in the response headers. + /// This may be useful for troubleshooting or when contacting OpenAI support in reference to a specific request. /// [JsonIgnore] public string RequestId { get; internal set; } @@ -68,12 +70,69 @@ public abstract class BaseResponse [JsonIgnore] public string ResetRequests { get; internal set; } + /// + /// The time until the rate limit (based on requests) resets to its initial state represented as a TimeSpan. + /// + [JsonIgnore] + public TimeSpan ResetRequestsTimespan => ConvertTimestampToTimespan(ResetTokens); + /// /// The time until the rate limit (based on tokens) resets to its initial state. /// [JsonIgnore] public string ResetTokens { get; internal set; } + /// + /// The time until the rate limit (based on tokens) resets to its initial state represented as a TimeSpan. + /// + [JsonIgnore] + public TimeSpan ResetTokensTimespan => ConvertTimestampToTimespan(ResetTokens); + + /* + * Regex Notes: + * The gist of this regex is that it is searching for "timestamp segments", e.g. 1m or 144ms. + * Each segment gets matched into its respective named capture group, from which we further parse out the + * digits. This allows us to take the string 6m45s99ms and insert the integers into a + * TimeSpan object for easier use. + * + * Regex Performance Notes, against 100k randomly generated timestamps: + * Average performance: 0.0003ms + * Best case: 0ms + * Worst Case: 15ms + * Total Time: 30ms + * + * Inconsequential compute time + */ + private readonly Regex timestampRegex = new Regex(@"^(?\d+h)?(?\d+m(?!s))?(?\d+s)?(?\d+ms)?"); + + /// + /// Takes a timestamp received from a OpenAI response header and converts to a TimeSpan + /// + /// The timestamp received from an OpenAI header, e.g. x-ratelimit-reset-tokens + /// A TimeSpan that represents the timestamp provided + /// Thrown if the provided timestamp is not in the expected format, or if the match is not successful. + private TimeSpan ConvertTimestampToTimespan(string timestamp) + { + var match = timestampRegex.Match(timestamp); + + if (!match.Success) + { + throw new ArgumentException($"Could not parse timestamp header. '{timestamp}'."); + } + + /* + * Note about Hours in timestamps: + * I have not personally observed a timestamp with an hours segment (e.g. 1h30m15s1ms). + * Although their presence may not actually exist, we can still have this section in the parser, there is no + * negative impact for a missing hours segment because the capture groups are flagged as optional. + */ + int.TryParse(match.Groups["h"].Value.Replace("h", string.Empty), out var h); + int.TryParse(match.Groups["m"].Value.Replace("m", string.Empty), out var m); + int.TryParse(match.Groups["s"].Value.Replace("s", string.Empty), out var s); + int.TryParse(match.Groups["ms"].Value.Replace("ms", string.Empty), out var ms); + return new TimeSpan(h, m, s) + TimeSpan.FromMilliseconds(ms); + } + public string ToJsonString() => this.ToEscapedJsonString(); } diff --git a/OpenAI-DotNet/Common/FileSearchOptions.cs b/OpenAI-DotNet/Common/FileSearchOptions.cs index 14818794..c85a9bfe 100644 --- a/OpenAI-DotNet/Common/FileSearchOptions.cs +++ b/OpenAI-DotNet/Common/FileSearchOptions.cs @@ -9,7 +9,7 @@ public sealed class FileSearchOptions { public FileSearchOptions() { } - public FileSearchOptions(int maxNumberOfResults) + public FileSearchOptions(int maxNumberOfResults, RankingOptions rankingOptions = null) { MaxNumberOfResults = maxNumberOfResults switch { @@ -17,10 +17,15 @@ public FileSearchOptions(int maxNumberOfResults) > 50 => throw new ArgumentOutOfRangeException(nameof(maxNumberOfResults), "Max number of results must be less than 50."), _ => maxNumberOfResults }; + RankingOptions = rankingOptions ?? new RankingOptions(); } [JsonInclude] [JsonPropertyName("max_num_results")] public int MaxNumberOfResults { get; private set; } + + [JsonInclude] + [JsonPropertyName("ranking_options")] + public RankingOptions RankingOptions { get; private set; } } } diff --git a/OpenAI-DotNet/Common/RankingOptions.cs b/OpenAI-DotNet/Common/RankingOptions.cs new file mode 100644 index 00000000..e1c03b99 --- /dev/null +++ b/OpenAI-DotNet/Common/RankingOptions.cs @@ -0,0 +1,50 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Text.Json.Serialization; + +namespace OpenAI +{ + /// + /// The ranking options for the file search. + /// + /// + public sealed class RankingOptions + { + /// + /// Constructor. + /// + /// + /// The ranker to use for the file search. + /// If not specified will use the `auto` ranker. + /// + /// + /// The score threshold for the file search. + /// All values must be a floating point number between 0 and 1. + /// + /// + [JsonConstructor] + public RankingOptions(string ranker = "auto", float scoreThreshold = 0f) + { + Ranker = ranker; + ScoreThreshold = scoreThreshold switch + { + < 0 => throw new ArgumentOutOfRangeException(nameof(scoreThreshold), "Score threshold must be greater than or equal to 0."), + > 1 => throw new ArgumentOutOfRangeException(nameof(scoreThreshold), "Score threshold must be less than or equal to 1."), + _ => scoreThreshold + }; + } + + /// + /// The ranker to use for the file search. + /// + [JsonPropertyName("ranker")] + public string Ranker { get; } + + /// + /// The score threshold for the file search. + /// + [JsonPropertyName("score_threshold")] + public float ScoreThreshold { get; } + } +} diff --git a/OpenAI-DotNet/Extensions/ResponseFormatConverter.cs b/OpenAI-DotNet/Extensions/ResponseFormatConverter.cs new file mode 100644 index 00000000..68dccbad --- /dev/null +++ b/OpenAI-DotNet/Extensions/ResponseFormatConverter.cs @@ -0,0 +1,34 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenAI.Extensions +{ + internal sealed class ResponseFormatConverter : JsonConverter + { + public override ResponseFormatObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + if (reader.TokenType is JsonTokenType.Null or JsonTokenType.String) + { + return ChatResponseFormat.Auto; + } + + return JsonSerializer.Deserialize(ref reader, options); + } + catch (Exception e) + { + throw new Exception($"Error reading {typeof(ChatResponseFormat)} from JSON.", e); + } + } + + public override void Write(Utf8JsonWriter writer, ResponseFormatObject value, JsonSerializerOptions options) + { + // serialize the object normally + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/OpenAI-DotNet/Files/FilesEndpoint.cs b/OpenAI-DotNet/Files/FilesEndpoint.cs index 16968c8d..b06cf1ad 100644 --- a/OpenAI-DotNet/Files/FilesEndpoint.cs +++ b/OpenAI-DotNet/Files/FilesEndpoint.cs @@ -87,14 +87,22 @@ public async Task UploadFileAsync(string filePath, string purpose, /// . public async Task UploadFileAsync(FileUploadRequest request, CancellationToken cancellationToken = default) { - using var fileData = new MemoryStream(); - using var content = new MultipartFormDataContent(); - await request.File.CopyToAsync(fileData, cancellationToken).ConfigureAwait(false); - content.Add(new StringContent(request.Purpose), "purpose"); - content.Add(new ByteArrayContent(fileData.ToArray()), "file", request.FileName); - request.Dispose(); - using var response = await client.Client.PostAsync(GetUrl(), content, cancellationToken).ConfigureAwait(false); - var responseAsString = await response.ReadAsStringAsync(EnableDebug, content, cancellationToken).ConfigureAwait(false); + using var payload = new MultipartFormDataContent(); + + try + { + using var fileData = new MemoryStream(); + await request.File.CopyToAsync(fileData, cancellationToken).ConfigureAwait(false); + payload.Add(new StringContent(request.Purpose), "purpose"); + payload.Add(new ByteArrayContent(fileData.ToArray()), "file", request.FileName); + } + finally + { + request.Dispose(); + } + + using var response = await client.Client.PostAsync(GetUrl(), payload, cancellationToken).ConfigureAwait(false); + var responseAsString = await response.ReadAsStringAsync(EnableDebug, payload, cancellationToken).ConfigureAwait(false); return response.Deserialize(responseAsString, client); } diff --git a/OpenAI-DotNet/Images/ImagesEndpoint.cs b/OpenAI-DotNet/Images/ImagesEndpoint.cs index 079bd705..2801d77b 100644 --- a/OpenAI-DotNet/Images/ImagesEndpoint.cs +++ b/OpenAI-DotNet/Images/ImagesEndpoint.cs @@ -1,6 +1,7 @@ // 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; @@ -45,31 +46,38 @@ public async Task> GenerateImageAsync(ImageGeneration /// A list of generated texture urls to download. public async Task> CreateImageEditAsync(ImageEditRequest request, CancellationToken cancellationToken = default) { - using var content = new MultipartFormDataContent(); - using var imageData = new MemoryStream(); - await request.Image.CopyToAsync(imageData, cancellationToken).ConfigureAwait(false); - content.Add(new ByteArrayContent(imageData.ToArray()), "image", request.ImageName); + using var payload = new MultipartFormDataContent(); - if (request.Mask != null) + try { - using var maskData = new MemoryStream(); - await request.Mask.CopyToAsync(maskData, cancellationToken).ConfigureAwait(false); - content.Add(new ByteArrayContent(maskData.ToArray()), "mask", request.MaskName); + using var imageData = new MemoryStream(); + await request.Image.CopyToAsync(imageData, cancellationToken).ConfigureAwait(false); + payload.Add(new ByteArrayContent(imageData.ToArray()), "image", request.ImageName); + + if (request.Mask != null) + { + using var maskData = new MemoryStream(); + await request.Mask.CopyToAsync(maskData, cancellationToken).ConfigureAwait(false); + payload.Add(new ByteArrayContent(maskData.ToArray()), "mask", request.MaskName); + } + + payload.Add(new StringContent(request.Prompt), "prompt"); + payload.Add(new StringContent(request.Number.ToString()), "n"); + payload.Add(new StringContent(request.Size), "size"); + payload.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); + + if (!string.IsNullOrWhiteSpace(request.User)) + { + payload.Add(new StringContent(request.User), "user"); + } } - - content.Add(new StringContent(request.Prompt), "prompt"); - content.Add(new StringContent(request.Number.ToString()), "n"); - content.Add(new StringContent(request.Size), "size"); - content.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); - - if (!string.IsNullOrWhiteSpace(request.User)) + finally { - content.Add(new StringContent(request.User), "user"); + request.Dispose(); } - request.Dispose(); - using var response = await client.Client.PostAsync(GetUrl("/edits"), content, cancellationToken).ConfigureAwait(false); - return await DeserializeResponseAsync(response, content, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/edits"), payload, cancellationToken).ConfigureAwait(false); + return await DeserializeResponseAsync(response, payload, cancellationToken).ConfigureAwait(false); } /// @@ -80,22 +88,29 @@ public async Task> CreateImageEditAsync(ImageEditRequ /// A list of generated texture urls to download. public async Task> CreateImageVariationAsync(ImageVariationRequest request, CancellationToken cancellationToken = default) { - using var content = new MultipartFormDataContent(); - using var imageData = new MemoryStream(); - await request.Image.CopyToAsync(imageData, cancellationToken).ConfigureAwait(false); - content.Add(new ByteArrayContent(imageData.ToArray()), "image", request.ImageName); - content.Add(new StringContent(request.Number.ToString()), "n"); - content.Add(new StringContent(request.Size), "size"); - content.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); - - if (!string.IsNullOrWhiteSpace(request.User)) + using var payload = new MultipartFormDataContent(); + + try { - content.Add(new StringContent(request.User), "user"); + using var imageData = new MemoryStream(); + await request.Image.CopyToAsync(imageData, cancellationToken).ConfigureAwait(false); + payload.Add(new ByteArrayContent(imageData.ToArray()), "image", request.ImageName); + payload.Add(new StringContent(request.Number.ToString()), "n"); + payload.Add(new StringContent(request.Size), "size"); + payload.Add(new StringContent(request.ResponseFormat.ToString().ToLower()), "response_format"); + + if (!string.IsNullOrWhiteSpace(request.User)) + { + payload.Add(new StringContent(request.User), "user"); + } + } + finally + { + request.Dispose(); } - request.Dispose(); - using var response = await client.Client.PostAsync(GetUrl("/variations"), content, cancellationToken).ConfigureAwait(false); - return await DeserializeResponseAsync(response, content, cancellationToken).ConfigureAwait(false); + using var response = await client.Client.PostAsync(GetUrl("/variations"), payload, cancellationToken).ConfigureAwait(false); + return await DeserializeResponseAsync(response, payload, cancellationToken).ConfigureAwait(false); } private async Task> DeserializeResponseAsync(HttpResponseMessage response, HttpContent requestContent, CancellationToken cancellationToken = default) diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index a1005ba2..93d16fd3 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -29,8 +29,13 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx true true - 8.2.2 + 8.2.4 +Version 8.2.4 +- Fixed ResponseObjectFormat deserialization when set to auto +- Added RankingOptions to FileSearchOptions +- Fixed potential memory leaks when uploading files to various endpoints +- Added timestamp values to BaseResponse to calculate rate limits Version 8.2.2 - Added generic parameters to methods that support structured output Version 8.2.1 diff --git a/OpenAI-DotNet/Threads/CreateRunRequest.cs b/OpenAI-DotNet/Threads/CreateRunRequest.cs index 89e01b8e..7e1326a1 100644 --- a/OpenAI-DotNet/Threads/CreateRunRequest.cs +++ b/OpenAI-DotNet/Threads/CreateRunRequest.cs @@ -1,5 +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.Linq; @@ -317,6 +318,7 @@ public CreateRunRequest( /// which indicates the generation exceeded max_tokens or the conversation exceeded the max context length. /// [JsonPropertyName("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ResponseFormatObject ResponseFormatObject { get; internal set; } diff --git a/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs b/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs index 1b4d12fc..94c68909 100644 --- a/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs +++ b/OpenAI-DotNet/Threads/CreateThreadAndRunRequest.cs @@ -1,5 +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.Linq; @@ -319,6 +320,7 @@ public CreateThreadAndRunRequest( /// which indicates the generation exceeded max_tokens or the conversation exceeded the max context length. /// [JsonPropertyName("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ResponseFormatObject ResponseFormatObject { get; internal set; } diff --git a/OpenAI-DotNet/Threads/RunResponse.cs b/OpenAI-DotNet/Threads/RunResponse.cs index 18509120..e12797b4 100644 --- a/OpenAI-DotNet/Threads/RunResponse.cs +++ b/OpenAI-DotNet/Threads/RunResponse.cs @@ -280,6 +280,7 @@ public IReadOnlyList Tools /// [JsonInclude] [JsonPropertyName("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ResponseFormatObject ResponseFormatObject { get; private set; }