Skip to content

Commit

Permalink
com.openai.unity 8.2.4 (#294)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
StephenHodgson authored Sep 14, 2024
1 parent 260d6dd commit e6c5e97
Show file tree
Hide file tree
Showing 17 changed files with 313 additions and 85 deletions.
4 changes: 3 additions & 1 deletion Runtime/Assistants/AssistantResponse.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -28,7 +29,7 @@ internal AssistantResponse(
[JsonProperty("metadata")] Dictionary<string, string> 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;
Expand Down Expand Up @@ -172,6 +173,7 @@ internal AssistantResponse(
/// </remarks>
[Preserve]
[JsonProperty("response_format")]
[JsonConverter(typeof(ResponseFormatConverter))]
public ResponseFormatObject ResponseFormatObject { get; }

[JsonIgnore]
Expand Down
2 changes: 2 additions & 0 deletions Runtime/Assistants/CreateAssistantRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using Newtonsoft.Json;
using Newtonsoft.Json.Schema;
using OpenAI.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -289,6 +290,7 @@ public CreateAssistantRequest(
/// which indicates the generation exceeded max_tokens or the conversation exceeded the max context length.
/// </remarks>
[Preserve]
[JsonConverter(typeof(ResponseFormatConverter))]
[JsonProperty("response_format", DefaultValueHandling = DefaultValueHandling.Ignore)]
public ResponseFormatObject ResponseFormatObject { get; internal set; }

Expand Down
94 changes: 53 additions & 41 deletions Runtime/Audio/AudioEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,41 +156,47 @@ public async Task<AudioResponse> CreateTranscriptionJsonAsync(AudioTranscription

private async Task<string> 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;
}
Expand Down Expand Up @@ -233,28 +239,34 @@ public async Task<AudioResponse> CreateTranslationJsonAsync(AudioTranslationRequ

private async Task<string> 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;
}
Expand Down
2 changes: 2 additions & 0 deletions Runtime/Chat/ChatRequest.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -287,6 +288,7 @@ public ChatRequest(
/// </remarks>
[Preserve]
[JsonProperty("response_format")]
[JsonConverter(typeof(ResponseFormatConverter))]
public ResponseFormatObject ResponseFormatObject { get; internal set; }

[JsonIgnore]
Expand Down
58 changes: 58 additions & 0 deletions Runtime/Common/BaseResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using Newtonsoft.Json;
using System;
using System.Text.RegularExpressions;

namespace OpenAI
{
Expand Down Expand Up @@ -67,12 +68,69 @@ public abstract class BaseResponse
[JsonIgnore]
public string ResetRequests { get; internal set; }

/// <summary>
/// The time until the rate limit (based on requests) resets to its initial state represented as a TimeSpan.
/// </summary>
[JsonIgnore]
public TimeSpan ResetRequestsTimespan => ConvertTimestampToTimespan(ResetTokens);

/// <summary>
/// The time until the rate limit (based on tokens) resets to its initial state.
/// </summary>
[JsonIgnore]
public string ResetTokens { get; internal set; }

/// <summary>
/// The time until the rate limit (based on tokens) resets to its initial state represented as a TimeSpan.
/// </summary>
[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(@"^(?<h>\d+h)?(?<m>\d+m(?!s))?(?<s>\d+s)?(?<ms>\d+ms)?");

/// <summary>
/// Takes a timestamp received from a OpenAI response header and converts to a TimeSpan
/// </summary>
/// <param name="timestamp">The timestamp received from an OpenAI header, e.g. x-ratelimit-reset-tokens</param>
/// <returns>A TimeSpan that represents the timestamp provided</returns>
/// <exception cref="ArgumentException">Thrown if the provided timestamp is not in the expected format, or if the match is not successful.</exception>
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);
}
Expand Down
17 changes: 15 additions & 2 deletions Runtime/Common/FileSearchOptions.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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; }
}
}
56 changes: 56 additions & 0 deletions Runtime/Common/RankingOptions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The ranking options for the file search.
/// <see href="https://platform.openai.com/docs/assistants/tools/file-search/customizing-file-search-settings"/>
/// </summary>
[Preserve]
public sealed class RankingOptions
{
/// <summary>
/// Constructor.
/// </summary>
/// <param name="ranker">
/// The ranker to use for the file search.
/// If not specified will use the `auto` ranker.
/// </param>
/// <param name="scoreThreshold">
/// The score threshold for the file search.
/// All values must be a floating point number between 0 and 1.
/// </param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[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
};
}

/// <summary>
/// The ranker to use for the file search.
/// </summary>
[Preserve]
[JsonProperty("ranker")]
public string Ranker { get; }

/// <summary>
/// The score threshold for the file search.
/// </summary>
[Preserve]
[JsonProperty("score_threshold", DefaultValueHandling = DefaultValueHandling.Include)]
public float ScoreThreshold { get; }
}
}
11 changes: 11 additions & 0 deletions Runtime/Common/RankingOptions.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e6c5e97

Please sign in to comment.