Skip to content

Commit

Permalink
OpenAI-DotNet 8.2.3
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
  • Loading branch information
StephenHodgson committed Sep 14, 2024
1 parent d79c4d4 commit 0a8a0d1
Show file tree
Hide file tree
Showing 14 changed files with 223 additions and 84 deletions.
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
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);
}
}
}
24 changes: 16 additions & 8 deletions OpenAI-DotNet/Files/FilesEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,22 @@ public async Task<FileResponse> UploadFileAsync(string filePath, string purpose,
/// <returns><see cref="FileResponse"/>.</returns>
public async Task<FileResponse> 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<FileResponse>(responseAsString, client);
}

Expand Down
Loading

0 comments on commit 0a8a0d1

Please sign in to comment.