From e6c5e9708741f0878d63ce75ecf649a8190ce738 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 14 Sep 2024 13:33:45 -0400 Subject: [PATCH] com.openai.unity 8.2.4 (#294) - 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 --- Runtime/Assistants/AssistantResponse.cs | 4 +- Runtime/Assistants/CreateAssistantRequest.cs | 2 + Runtime/Audio/AudioEndpoint.cs | 94 +++++++++++-------- Runtime/Chat/ChatRequest.cs | 2 + Runtime/Common/BaseResponse.cs | 58 ++++++++++++ Runtime/Common/FileSearchOptions.cs | 17 +++- Runtime/Common/RankingOptions.cs | 56 +++++++++++ Runtime/Common/RankingOptions.cs.meta | 11 +++ Runtime/Extensions/ResponseFormatConverter.cs | 36 +++++++ .../ResponseFormatConverter.cs.meta | 11 +++ Runtime/Files/FilesEndpoint.cs | 22 +++-- Runtime/Images/ImagesEndpoint.cs | 74 +++++++++------ Runtime/Threads/CreateRunRequest.cs | 2 + Runtime/Threads/CreateThreadAndRunRequest.cs | 2 + Runtime/Threads/RunResponse.cs | 3 +- Tests/TestFixture_02_Assistants.cs | 2 +- package.json | 2 +- 17 files changed, 313 insertions(+), 85 deletions(-) create mode 100644 Runtime/Common/RankingOptions.cs create mode 100644 Runtime/Common/RankingOptions.cs.meta create mode 100644 Runtime/Extensions/ResponseFormatConverter.cs create mode 100644 Runtime/Extensions/ResponseFormatConverter.cs.meta diff --git a/Runtime/Assistants/AssistantResponse.cs b/Runtime/Assistants/AssistantResponse.cs index 656185bd..8a33cd21 100644 --- a/Runtime/Assistants/AssistantResponse.cs +++ b/Runtime/Assistants/AssistantResponse.cs @@ -1,6 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using Newtonsoft.Json; +using OpenAI.Extensions; using System; using System.Collections.Generic; using UnityEngine.Scripting; @@ -28,7 +29,7 @@ internal AssistantResponse( [JsonProperty("metadata")] Dictionary metadata, [JsonProperty("temperature")] double temperature, [JsonProperty("top_p")] double topP, - [JsonProperty("response_format")] ResponseFormatObject responseFormat) + [JsonProperty("response_format")][JsonConverter(typeof(ResponseFormatConverter))] ResponseFormatObject responseFormat) { Id = id; Object = @object; @@ -172,6 +173,7 @@ internal AssistantResponse( /// [Preserve] [JsonProperty("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] public ResponseFormatObject ResponseFormatObject { get; } [JsonIgnore] diff --git a/Runtime/Assistants/CreateAssistantRequest.cs b/Runtime/Assistants/CreateAssistantRequest.cs index 458e4ae6..4f354ef3 100644 --- a/Runtime/Assistants/CreateAssistantRequest.cs +++ b/Runtime/Assistants/CreateAssistantRequest.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Schema; +using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -289,6 +290,7 @@ public CreateAssistantRequest( /// which indicates the generation exceeded max_tokens or the conversation exceeded the max context length. /// [Preserve] + [JsonConverter(typeof(ResponseFormatConverter))] [JsonProperty("response_format", DefaultValueHandling = DefaultValueHandling.Ignore)] public ResponseFormatObject ResponseFormatObject { get; internal set; } diff --git a/Runtime/Audio/AudioEndpoint.cs b/Runtime/Audio/AudioEndpoint.cs index b09cd4e7..83796697 100644 --- a/Runtime/Audio/AudioEndpoint.cs +++ b/Runtime/Audio/AudioEndpoint.cs @@ -156,41 +156,47 @@ public async Task CreateTranscriptionJsonAsync(AudioTranscription private async Task Internal_CreateTranscriptionAsync(AudioTranscriptionRequest request, CancellationToken cancellationToken = default) { - var form = new WWWForm(); - using var audioData = new MemoryStream(); - await request.Audio.CopyToAsync(audioData, cancellationToken); - form.AddBinaryData("file", audioData.ToArray(), request.AudioName); - form.AddField("model", request.Model); + var payload = new WWWForm(); - if (!string.IsNullOrWhiteSpace(request.Prompt)) + try { - form.AddField("prompt", request.Prompt); - } + using var audioData = new MemoryStream(); + await request.Audio.CopyToAsync(audioData, cancellationToken); + payload.AddBinaryData("file", audioData.ToArray(), request.AudioName); + payload.AddField("model", request.Model); - var responseFormat = request.ResponseFormat; - form.AddField("response_format", responseFormat.ToString().ToLower()); + if (!string.IsNullOrWhiteSpace(request.Prompt)) + { + payload.AddField("prompt", request.Prompt); + } - if (request.Temperature.HasValue) - { - form.AddField("temperature", request.Temperature.Value.ToString(CultureInfo.InvariantCulture)); - } + var responseFormat = request.ResponseFormat; + payload.AddField("response_format", responseFormat.ToString().ToLower()); - if (!string.IsNullOrWhiteSpace(request.Language)) - { - form.AddField("language", request.Language); - } + if (request.Temperature.HasValue) + { + payload.AddField("temperature", request.Temperature.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (!string.IsNullOrWhiteSpace(request.Language)) + { + payload.AddField("language", request.Language); + } - switch (request.TimestampGranularities) + switch (request.TimestampGranularities) + { + case TimestampGranularity.Segment: + case TimestampGranularity.Word: + payload.AddField("timestamp_granularities[]", request.TimestampGranularities.ToString().ToLower()); + break; + } + } + finally { - case TimestampGranularity.Segment: - case TimestampGranularity.Word: - form.AddField("timestamp_granularities[]", request.TimestampGranularities.ToString().ToLower()); - break; + request.Dispose(); } - request.Dispose(); - - var response = await Rest.PostAsync(GetUrl("/transcriptions"), form, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + var response = await Rest.PostAsync(GetUrl("/transcriptions"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); return response.Body; } @@ -233,28 +239,34 @@ public async Task CreateTranslationJsonAsync(AudioTranslationRequ private async Task Internal_CreateTranslationAsync(AudioTranslationRequest request, CancellationToken cancellationToken) { - var form = new WWWForm(); - using var audioData = new MemoryStream(); - await request.Audio.CopyToAsync(audioData, cancellationToken); - form.AddBinaryData("file", audioData.ToArray(), request.AudioName); - form.AddField("model", request.Model); + var payload = new WWWForm(); - if (!string.IsNullOrWhiteSpace(request.Prompt)) + try { - form.AddField("prompt", request.Prompt); - } + using var audioData = new MemoryStream(); + await request.Audio.CopyToAsync(audioData, cancellationToken); + payload.AddBinaryData("file", audioData.ToArray(), request.AudioName); + payload.AddField("model", request.Model); + + if (!string.IsNullOrWhiteSpace(request.Prompt)) + { + payload.AddField("prompt", request.Prompt); + } - var responseFormat = request.ResponseFormat; - form.AddField("response_format", responseFormat.ToString().ToLower()); + var responseFormat = request.ResponseFormat; + payload.AddField("response_format", responseFormat.ToString().ToLower()); - if (request.Temperature.HasValue) + if (request.Temperature.HasValue) + { + payload.AddField("temperature", request.Temperature.Value.ToString(CultureInfo.InvariantCulture)); + } + } + finally { - form.AddField("temperature", request.Temperature.Value.ToString(CultureInfo.InvariantCulture)); + request.Dispose(); } - request.Dispose(); - - var response = await Rest.PostAsync(GetUrl("/translations"), form, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + var response = await Rest.PostAsync(GetUrl("/translations"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); response.Validate(EnableDebug); return response.Body; } diff --git a/Runtime/Chat/ChatRequest.cs b/Runtime/Chat/ChatRequest.cs index 14b29871..bded4eed 100644 --- a/Runtime/Chat/ChatRequest.cs +++ b/Runtime/Chat/ChatRequest.cs @@ -1,6 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using Newtonsoft.Json; +using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -287,6 +288,7 @@ public ChatRequest( /// [Preserve] [JsonProperty("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] public ResponseFormatObject ResponseFormatObject { get; internal set; } [JsonIgnore] diff --git a/Runtime/Common/BaseResponse.cs b/Runtime/Common/BaseResponse.cs index 50eef42f..7495d980 100644 --- a/Runtime/Common/BaseResponse.cs +++ b/Runtime/Common/BaseResponse.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using System; +using System.Text.RegularExpressions; namespace OpenAI { @@ -67,12 +68,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() => JsonConvert.SerializeObject(this, OpenAIClient.JsonSerializationOptions); } diff --git a/Runtime/Common/FileSearchOptions.cs b/Runtime/Common/FileSearchOptions.cs index 0d07c363..3f7f6de5 100644 --- a/Runtime/Common/FileSearchOptions.cs +++ b/Runtime/Common/FileSearchOptions.cs @@ -1,6 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using Newtonsoft.Json; +using System; using UnityEngine.Scripting; namespace OpenAI @@ -10,13 +11,25 @@ public sealed class FileSearchOptions { [Preserve] [JsonConstructor] - public FileSearchOptions(int maxNumberOfResults) + public FileSearchOptions( + [JsonProperty("max_num_results")] int maxNumberOfResults, + [JsonProperty("ranking_options")] RankingOptions rankingOptions = null) { - MaxNumberOfResults = maxNumberOfResults; + MaxNumberOfResults = maxNumberOfResults switch + { + < 1 => throw new ArgumentOutOfRangeException(nameof(maxNumberOfResults), "Max number of results must be greater than 0."), + > 50 => throw new ArgumentOutOfRangeException(nameof(maxNumberOfResults), "Max number of results must be less than 50."), + _ => maxNumberOfResults + }; + RankingOptions = rankingOptions ?? new RankingOptions(); } [Preserve] [JsonProperty("max_num_results")] public int MaxNumberOfResults { get; } + + [Preserve] + [JsonProperty("ranking_options")] + public RankingOptions RankingOptions { get; } } } diff --git a/Runtime/Common/RankingOptions.cs b/Runtime/Common/RankingOptions.cs new file mode 100644 index 00000000..5a4d1ab4 --- /dev/null +++ b/Runtime/Common/RankingOptions.cs @@ -0,0 +1,56 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System; +using UnityEngine.Scripting; + +namespace OpenAI +{ + /// + /// The ranking options for the file search. + /// + /// + [Preserve] + 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( + [JsonProperty("ranker")] string ranker = "auto", + [JsonProperty("score_threshold")] 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. + /// + [Preserve] + [JsonProperty("ranker")] + public string Ranker { get; } + + /// + /// The score threshold for the file search. + /// + [Preserve] + [JsonProperty("score_threshold", DefaultValueHandling = DefaultValueHandling.Include)] + public float ScoreThreshold { get; } + } +} diff --git a/Runtime/Common/RankingOptions.cs.meta b/Runtime/Common/RankingOptions.cs.meta new file mode 100644 index 00000000..5b3860e5 --- /dev/null +++ b/Runtime/Common/RankingOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ebd0e8d5090ea8541a2b6db74659b04b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Extensions/ResponseFormatConverter.cs b/Runtime/Extensions/ResponseFormatConverter.cs new file mode 100644 index 00000000..985cc617 --- /dev/null +++ b/Runtime/Extensions/ResponseFormatConverter.cs @@ -0,0 +1,36 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Newtonsoft.Json; +using System; +using UnityEngine.Scripting; + +namespace OpenAI.Extensions +{ + [Preserve] + internal sealed class ResponseFormatConverter : JsonConverter + { + [Preserve] + public override ResponseFormatObject ReadJson(JsonReader reader, Type objectType, ResponseFormatObject existingValue, bool hasExistingValue, JsonSerializer serializer) + { + try + { + if (reader.TokenType is JsonToken.Null or JsonToken.String) + { + return ChatResponseFormat.Auto; + } + + return serializer.Deserialize(reader); + } + catch (Exception e) + { + throw new JsonSerializationException($"Error reading {nameof(ChatResponseFormat)} from JSON", e); + } + } + + [Preserve] + public override void WriteJson(JsonWriter writer, ResponseFormatObject value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } + } +} diff --git a/Runtime/Extensions/ResponseFormatConverter.cs.meta b/Runtime/Extensions/ResponseFormatConverter.cs.meta new file mode 100644 index 00000000..8a7cdbe9 --- /dev/null +++ b/Runtime/Extensions/ResponseFormatConverter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b7230e39f4db6840b976f4f9ebfb875 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Files/FilesEndpoint.cs b/Runtime/Files/FilesEndpoint.cs index 9d9e32cf..da505fc0 100644 --- a/Runtime/Files/FilesEndpoint.cs +++ b/Runtime/Files/FilesEndpoint.cs @@ -98,13 +98,21 @@ public async Task UploadFileAsync(string filePath, string purpose, public async Task UploadFileAsync(FileUploadRequest request, IProgress uploadProgress = null, CancellationToken cancellationToken = default) { await Awaiters.UnityMainThread; - using var fileData = new MemoryStream(); - var content = new WWWForm(); - await request.File.CopyToAsync(fileData, cancellationToken); - content.AddField("purpose", request.Purpose); - content.AddBinaryData("file", fileData.ToArray(), request.FileName); - request.Dispose(); - var response = await Rest.PostAsync(GetUrl(), content, new RestParameters(client.DefaultRequestHeaders, uploadProgress), cancellationToken); + var payload = new WWWForm(); + + try + { + using var fileData = new MemoryStream(); + await request.File.CopyToAsync(fileData, cancellationToken); + payload.AddField("purpose", request.Purpose); + payload.AddBinaryData("file", fileData.ToArray(), request.FileName); + } + finally + { + request.Dispose(); + } + + var response = await Rest.PostAsync(GetUrl(), payload, new RestParameters(client.DefaultRequestHeaders, uploadProgress), cancellationToken); response.Validate(EnableDebug); return response.Deserialize(client); } diff --git a/Runtime/Images/ImagesEndpoint.cs b/Runtime/Images/ImagesEndpoint.cs index 693af1a8..18e02529 100644 --- a/Runtime/Images/ImagesEndpoint.cs +++ b/Runtime/Images/ImagesEndpoint.cs @@ -51,31 +51,37 @@ public async Task> GenerateImageAsync(ImageGeneration [Function("Creates an edited or extended image given an original image and a prompt.")] public async Task> CreateImageEditAsync(ImageEditRequest request, CancellationToken cancellationToken = default) { - var form = new WWWForm(); - using var imageData = new MemoryStream(); - await request.Image.CopyToAsync(imageData, cancellationToken); - form.AddBinaryData("image", imageData.ToArray(), request.ImageName); + var payload = new WWWForm(); - if (request.Mask != null) + try { - using var maskData = new MemoryStream(); - await request.Mask.CopyToAsync(maskData, cancellationToken); - form.AddBinaryData("mask", maskData.ToArray(), request.MaskName); - } + using var imageData = new MemoryStream(); + await request.Image.CopyToAsync(imageData, cancellationToken); + payload.AddBinaryData("image", imageData.ToArray(), request.ImageName); + + if (request.Mask != null) + { + using var maskData = new MemoryStream(); + await request.Mask.CopyToAsync(maskData, cancellationToken); + payload.AddBinaryData("mask", maskData.ToArray(), request.MaskName); + } - form.AddField("prompt", request.Prompt); - form.AddField("n", request.Number.ToString()); - form.AddField("size", request.Size); - form.AddField("response_format", request.ResponseFormat.ToString().ToLower()); + payload.AddField("prompt", request.Prompt); + payload.AddField("n", request.Number.ToString()); + payload.AddField("size", request.Size); + payload.AddField("response_format", request.ResponseFormat.ToString().ToLower()); - if (!string.IsNullOrWhiteSpace(request.User)) + if (!string.IsNullOrWhiteSpace(request.User)) + { + payload.AddField("user", request.User); + } + } + finally { - form.AddField("user", request.User); + request.Dispose(); } - request.Dispose(); - - var response = await Rest.PostAsync(GetUrl("/edits"), form, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + var response = await Rest.PostAsync(GetUrl("/edits"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); return await DeserializeResponseAsync(response, cancellationToken); } @@ -88,22 +94,28 @@ public async Task> CreateImageEditAsync(ImageEditRequ [Function("Creates a variation of a given image.")] public async Task> CreateImageVariationAsync(ImageVariationRequest request, CancellationToken cancellationToken = default) { - var form = new WWWForm(); - using var imageData = new MemoryStream(); - await request.Image.CopyToAsync(imageData, cancellationToken); - form.AddBinaryData("image", imageData.ToArray(), request.ImageName); - form.AddField("n", request.Number.ToString()); - form.AddField("size", request.Size); - form.AddField("response_format", request.ResponseFormat.ToString().ToLower()); - - if (!string.IsNullOrWhiteSpace(request.User)) + var payload = new WWWForm(); + + try { - form.AddField("user", request.User); + using var imageData = new MemoryStream(); + await request.Image.CopyToAsync(imageData, cancellationToken); + payload.AddBinaryData("image", imageData.ToArray(), request.ImageName); + payload.AddField("n", request.Number.ToString()); + payload.AddField("size", request.Size); + payload.AddField("response_format", request.ResponseFormat.ToString().ToLower()); + + if (!string.IsNullOrWhiteSpace(request.User)) + { + payload.AddField("user", request.User); + } + } + finally + { + request.Dispose(); } - request.Dispose(); - - var response = await Rest.PostAsync(GetUrl("/variations"), form, new RestParameters(client.DefaultRequestHeaders), cancellationToken); + var response = await Rest.PostAsync(GetUrl("/variations"), payload, new RestParameters(client.DefaultRequestHeaders), cancellationToken); return await DeserializeResponseAsync(response, cancellationToken); } diff --git a/Runtime/Threads/CreateRunRequest.cs b/Runtime/Threads/CreateRunRequest.cs index b8a35c98..384f5612 100644 --- a/Runtime/Threads/CreateRunRequest.cs +++ b/Runtime/Threads/CreateRunRequest.cs @@ -1,6 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using Newtonsoft.Json; +using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -331,6 +332,7 @@ public CreateRunRequest( /// [Preserve] [JsonProperty("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] public ResponseFormatObject ResponseFormatObject { get; internal set; } [JsonIgnore] diff --git a/Runtime/Threads/CreateThreadAndRunRequest.cs b/Runtime/Threads/CreateThreadAndRunRequest.cs index 98644bd6..0f485a0a 100644 --- a/Runtime/Threads/CreateThreadAndRunRequest.cs +++ b/Runtime/Threads/CreateThreadAndRunRequest.cs @@ -1,6 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using Newtonsoft.Json; +using OpenAI.Extensions; using System; using System.Collections.Generic; using System.Linq; @@ -332,6 +333,7 @@ public CreateThreadAndRunRequest( /// [Preserve] [JsonProperty("response_format")] + [JsonConverter(typeof(ResponseFormatConverter))] public ResponseFormatObject ResponseFormatObject { get; internal set; } [JsonIgnore] diff --git a/Runtime/Threads/RunResponse.cs b/Runtime/Threads/RunResponse.cs index 9241a62b..81f1b5bc 100644 --- a/Runtime/Threads/RunResponse.cs +++ b/Runtime/Threads/RunResponse.cs @@ -49,7 +49,7 @@ internal RunResponse( [JsonProperty("truncation_strategy")] TruncationStrategy truncationStrategy, [JsonProperty("tool_choice")] object toolChoice, [JsonProperty("parallel_tool_calls")] bool parallelToolCalls, - [JsonProperty("response_format")] ResponseFormatObject responseFormat) + [JsonProperty("response_format")][JsonConverter(typeof(ResponseFormatConverter))] ResponseFormatObject responseFormat) { Id = id; Object = @object; @@ -326,6 +326,7 @@ public DateTime? CompletedAt /// which indicates the generation exceeded max_tokens or the conversation exceeded the max context length. /// [Preserve] + [JsonConverter(typeof(ResponseFormatConverter))] [JsonProperty("response_format", DefaultValueHandling = DefaultValueHandling.Ignore)] public ResponseFormatObject ResponseFormatObject { get; private set; } diff --git a/Tests/TestFixture_02_Assistants.cs b/Tests/TestFixture_02_Assistants.cs index e001ab8d..94401eaf 100644 --- a/Tests/TestFixture_02_Assistants.cs +++ b/Tests/TestFixture_02_Assistants.cs @@ -49,7 +49,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/package.json b/package.json index 015d8092..9e48c80d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "OpenAI", "description": "A OpenAI package for the Unity Game Engine to use GPT-4, GPT-3.5, GPT-3 and Dall-E though their RESTful API (currently in beta).\n\nIndependently developed, this is not an official library and I am not affiliated with OpenAI.\n\nAn OpenAI API account is required.", "keywords": [], - "version": "8.2.3", + "version": "8.2.4", "unity": "2021.3", "documentationUrl": "https://github.com/RageAgainstThePixel/com.openai.unity#documentation", "changelogUrl": "https://github.com/RageAgainstThePixel/com.openai.unity/releases",