Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAI-DotNet 8.2.4 #365

Merged
merged 5 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion OpenAI-DotNet-Tests/TestFixture_02_Assistants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions OpenAI-DotNet/Assistants/AssistantResponse.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -136,6 +137,7 @@ public sealed class AssistantResponse : BaseResponse
/// </remarks>
[JsonInclude]
[JsonPropertyName("response_format")]
[JsonConverter(typeof(ResponseFormatConverter))]
public ResponseFormatObject ResponseFormatObject { get; private set; }

[JsonIgnore]
Expand Down
2 changes: 2 additions & 0 deletions OpenAI-DotNet/Assistants/CreateAssistantRequest.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -275,6 +276,7 @@ public CreateAssistantRequest(
/// which indicates the generation exceeded max_tokens or the conversation exceeded the max context length.
/// </remarks>
[JsonPropertyName("response_format")]
[JsonConverter(typeof(ResponseFormatConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public ResponseFormatObject ResponseFormatObject { get; internal set; }

Expand Down
94 changes: 53 additions & 41 deletions OpenAI-DotNet/Audio/AudioEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,41 +100,47 @@ public async Task<AudioResponse> 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);
}

Expand Down Expand Up @@ -172,28 +178,34 @@ public async Task<AudioResponse> 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);
}
}
Expand Down
2 changes: 2 additions & 0 deletions OpenAI-DotNet/Chat/ChatRequest.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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; }

Expand Down
61 changes: 60 additions & 1 deletion OpenAI-DotNet/Common/BaseResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using OpenAI.Extensions;
using System;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace OpenAI
{
Expand All @@ -27,7 +28,8 @@ public abstract class BaseResponse
public string Organization { get; internal set; }

/// <summary>
/// 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.
/// </summary>
[JsonIgnore]
public string RequestId { get; internal set; }
Expand Down Expand Up @@ -68,12 +70,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()
=> this.ToEscapedJsonString<object>();
}
Expand Down
7 changes: 6 additions & 1 deletion OpenAI-DotNet/Common/FileSearchOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@ public sealed class FileSearchOptions
{
public FileSearchOptions() { }

public FileSearchOptions(int maxNumberOfResults)
public FileSearchOptions(int maxNumberOfResults, RankingOptions rankingOptions = null)
{
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();
}

[JsonInclude]
[JsonPropertyName("max_num_results")]
public int MaxNumberOfResults { get; private set; }

[JsonInclude]
[JsonPropertyName("ranking_options")]
public RankingOptions RankingOptions { get; private set; }
}
}
50 changes: 50 additions & 0 deletions OpenAI-DotNet/Common/RankingOptions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The ranking options for the file search.
/// <see href="https://platform.openai.com/docs/assistants/tools/file-search/customizing-file-search-settings"/>
/// </summary>
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(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
};
}

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

/// <summary>
/// The score threshold for the file search.
/// </summary>
[JsonPropertyName("score_threshold")]
public float ScoreThreshold { get; }
}
}
34 changes: 34 additions & 0 deletions OpenAI-DotNet/Extensions/ResponseFormatConverter.cs
Original file line number Diff line number Diff line change
@@ -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<ResponseFormatObject>
{
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<ResponseFormatObject>(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);
}
}
}
Loading