From 46f31a5e781556e9645addc27e764aaeefbc8dac Mon Sep 17 00:00:00 2001 From: Jose Arriaga Maldonado Date: Sun, 1 Sep 2024 15:15:34 -0700 Subject: [PATCH] Fix deserialization of Run Steps when using File Search --- CHANGELOG.md | 4 ++ api/OpenAI.netstandard2.0.cs | 28 ++++++--- examples/CombinationExamples.cs | 6 +- src/Custom/Audio/GeneratedSpeechFormat.cs | 29 +-------- src/Custom/Audio/SpeechGenerationOptions.cs | 22 ++++--- .../GeneratedSpeechFormat.Serialization.cs | 33 ---------- src/Generated/Models/GeneratedSpeechFormat.cs | 44 +++++++++++++ ...atchRequestOutputResponse.Serialization.cs | 29 +++++++-- .../InternalBatchRequestOutputResponse.cs | 6 +- ...ToolCallsFileSearchObject.Serialization.cs | 27 ++++++-- ...ltaStepDetailsToolCallsFileSearchObject.cs | 6 +- ...FileSearchToolCallDetails.Serialization.cs | 27 ++++++-- ...nternalRunStepFileSearchToolCallDetails.cs | 6 +- .../SpeechGenerationOptions.Serialization.cs | 8 +-- .../Models/SpeechGenerationOptions.cs | 5 +- tests/Assistants/AssistantTests.cs | 63 ++++++++++++++++++- tests/Audio/TextToSpeechTests.cs | 32 +++++++--- 17 files changed, 253 insertions(+), 122 deletions(-) delete mode 100644 src/Generated/Models/GeneratedSpeechFormat.Serialization.cs create mode 100644 src/Generated/Models/GeneratedSpeechFormat.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d6940c24..d94e4c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,13 @@ - Updated fine-tuning pagination methods `GetJobs`, `GetEvents`, and `GetJobCheckpoints` to return `IEnumerable` instead of `ClientResult`. (commit_hash) - Updated the batching pagination method `GetBatches` to return `IEnumerable` instead of `ClientResult`. (commit_hash) - Changed `GeneratedSpeechVoice` from an enum to an "extensible enum". (commit_hash) +- Changed `GeneratedSpeechFormat` from an enum to an "extensible enum". (commit_hash) +- Renamed `SpeechGenerationOptions`'s `Speed` property to `SpeedRatio`. (commit_hash) ### Bugs Fixed +- Corrected an internal deserialization issue that caused recent updates to Assistants `file_search` to fail when streaming a run. Strongly typed support for `ranking_options` is not included but will arrive soon. (commit_hash) + ### Other Changes - Reverted the removal of the version path parameter "v1" from the default endpoint URL. (commit_hash) diff --git a/api/OpenAI.netstandard2.0.cs b/api/OpenAI.netstandard2.0.cs index 87abb883..b21008e0 100644 --- a/api/OpenAI.netstandard2.0.cs +++ b/api/OpenAI.netstandard2.0.cs @@ -1084,13 +1084,25 @@ public class AudioTranslationOptions : IJsonModel, IPer string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options); BinaryData IPersistableModel.Write(ModelReaderWriterOptions options); } - public enum GeneratedSpeechFormat { - Mp3 = 0, - Opus = 1, - Aac = 2, - Flac = 3, - Wav = 4, - Pcm = 5 + public readonly partial struct GeneratedSpeechFormat : IEquatable { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public GeneratedSpeechFormat(string value); + public static GeneratedSpeechFormat Aac { get; } + public static GeneratedSpeechFormat Flac { get; } + public static GeneratedSpeechFormat Mp3 { get; } + public static GeneratedSpeechFormat Opus { get; } + public static GeneratedSpeechFormat Pcm { get; } + public static GeneratedSpeechFormat Wav { get; } + public readonly bool Equals(GeneratedSpeechFormat other); + [EditorBrowsable(EditorBrowsableState.Never)] + public override readonly bool Equals(object obj); + [EditorBrowsable(EditorBrowsableState.Never)] + public override readonly int GetHashCode(); + public static bool operator ==(GeneratedSpeechFormat left, GeneratedSpeechFormat right); + public static implicit operator GeneratedSpeechFormat(string value); + public static bool operator !=(GeneratedSpeechFormat left, GeneratedSpeechFormat right); + public override readonly string ToString(); } public readonly partial struct GeneratedSpeechVoice : IEquatable { private readonly object _dummy; @@ -1120,7 +1132,7 @@ public static class OpenAIAudioModelFactory { } public class SpeechGenerationOptions : IJsonModel, IPersistableModel { public GeneratedSpeechFormat? ResponseFormat { get; set; } - public float? Speed { get; set; } + public float? SpeedRatio { get; set; } SpeechGenerationOptions IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options); void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options); SpeechGenerationOptions IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options); diff --git a/examples/CombinationExamples.cs b/examples/CombinationExamples.cs index 7987d81f..ffa800c3 100644 --- a/examples/CombinationExamples.cs +++ b/examples/CombinationExamples.cs @@ -53,7 +53,7 @@ public void AlpacaArtAssessor() GeneratedSpeechVoice.Fable, new SpeechGenerationOptions() { - Speed = 0.9f, + SpeedRatio = 0.9f, ResponseFormat = GeneratedSpeechFormat.Opus, }); FileInfo ttsFileInfo = new($"{chatCompletion.Id}.opus"); @@ -89,7 +89,7 @@ public async Task CuriousCreatureCreator() GeneratedSpeechVoice.Onyx, new SpeechGenerationOptions() { - Speed = 1.1f, + SpeedRatio = 1.1f, ResponseFormat = GeneratedSpeechFormat.Opus, }); _ = Task.Run(async () => @@ -136,7 +136,7 @@ public async Task CuriousCreatureCreator() GeneratedSpeechVoice.Fable, new SpeechGenerationOptions() { - Speed = 0.9f, + SpeedRatio = 0.9f, ResponseFormat = GeneratedSpeechFormat.Opus, }); FileInfo criticAudioFileInfo = new($"{criticalAppraisalResult.Value.Id}-appraisal.opus"); diff --git a/src/Custom/Audio/GeneratedSpeechFormat.cs b/src/Custom/Audio/GeneratedSpeechFormat.cs index 4ee71fef..8ef09705 100644 --- a/src/Custom/Audio/GeneratedSpeechFormat.cs +++ b/src/Custom/Audio/GeneratedSpeechFormat.cs @@ -1,32 +1,7 @@ namespace OpenAI.Audio; -/// -/// Represents an audio data format available as either input or output into an audio operation. -/// +/// The audio format in which to generate the speech. [CodeGenModel("CreateSpeechRequestResponseFormat")] -public enum GeneratedSpeechFormat +public readonly partial struct GeneratedSpeechFormat { - /// MP3. /// - [CodeGenMember("Mp3")] - Mp3, - - /// Opus. /// - [CodeGenMember("Opus")] - Opus, - - /// AAC (advanced audio coding). /// - [CodeGenMember("Aac")] - Aac, - - /// FLAC (free lossless audio codec). /// - [CodeGenMember("Flac")] - Flac, - - /// WAV. /// - [CodeGenMember("Wav")] - Wav, - - /// PCM (pulse-code modulation). /// - [CodeGenMember("Pcm")] - Pcm, } \ No newline at end of file diff --git a/src/Custom/Audio/SpeechGenerationOptions.cs b/src/Custom/Audio/SpeechGenerationOptions.cs index 7c67060f..387d3c5b 100644 --- a/src/Custom/Audio/SpeechGenerationOptions.cs +++ b/src/Custom/Audio/SpeechGenerationOptions.cs @@ -1,9 +1,6 @@ namespace OpenAI.Audio; -/// -/// A representation of additional options available to control the behavior of a text-to-speech audio generation -/// operation. -/// +/// The options to configure text-to-speech audio generation. [CodeGenModel("CreateSpeechRequest")] [CodeGenSuppress("SpeechGenerationOptions", typeof(InternalCreateSpeechRequestModel), typeof(string), typeof(GeneratedSpeechVoice))] public partial class SpeechGenerationOptions @@ -11,23 +8,20 @@ public partial class SpeechGenerationOptions // CUSTOM: // - Made internal. The model is specified by the client. // - Added setter. - /// One of the available [TTS models](/docs/models/tts): `tts-1` or `tts-1-hd`. + [CodeGenMember("Model")] internal InternalCreateSpeechRequestModel Model { get; set; } // CUSTOM: // - Made internal. This value comes from a parameter on the client method. // - Added setter. /// The text to generate audio for. The maximum length is 4096 characters. + [CodeGenMember("Input")] internal string Input { get; set; } // CUSTOM: // - Made internal. This value comes from a parameter on the client method. // - Added setter. - /// - /// The voice to use when generating the audio. Supported voices are `alloy`, `echo`, `fable`, - /// `onyx`, `nova`, and `shimmer`. Previews of the voices are available in the - /// [Text to speech guide](/docs/guides/text-to-speech/voice-options). - /// + [CodeGenMember("Voice")] internal GeneratedSpeechVoice Voice { get; set; } // CUSTOM: Made public now that there are no required properties. @@ -35,4 +29,12 @@ public partial class SpeechGenerationOptions public SpeechGenerationOptions() { } + + // CUSTOM: Renamed. + /// + /// The speed of the generated audio expressed as a ratio between 0.5 and 2.0. The default is 1.0. + /// + [CodeGenMember("Speed")] + + public float? SpeedRatio { get; set; } } \ No newline at end of file diff --git a/src/Generated/Models/GeneratedSpeechFormat.Serialization.cs b/src/Generated/Models/GeneratedSpeechFormat.Serialization.cs deleted file mode 100644 index ff1d45f8..00000000 --- a/src/Generated/Models/GeneratedSpeechFormat.Serialization.cs +++ /dev/null @@ -1,33 +0,0 @@ -// - -#nullable disable - -using System; - -namespace OpenAI.Audio -{ - internal static partial class GeneratedSpeechFormatExtensions - { - public static string ToSerialString(this GeneratedSpeechFormat value) => value switch - { - GeneratedSpeechFormat.Mp3 => "mp3", - GeneratedSpeechFormat.Opus => "opus", - GeneratedSpeechFormat.Aac => "aac", - GeneratedSpeechFormat.Flac => "flac", - GeneratedSpeechFormat.Wav => "wav", - GeneratedSpeechFormat.Pcm => "pcm", - _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown GeneratedSpeechFormat value.") - }; - - public static GeneratedSpeechFormat ToGeneratedSpeechFormat(this string value) - { - if (StringComparer.OrdinalIgnoreCase.Equals(value, "mp3")) return GeneratedSpeechFormat.Mp3; - if (StringComparer.OrdinalIgnoreCase.Equals(value, "opus")) return GeneratedSpeechFormat.Opus; - if (StringComparer.OrdinalIgnoreCase.Equals(value, "aac")) return GeneratedSpeechFormat.Aac; - if (StringComparer.OrdinalIgnoreCase.Equals(value, "flac")) return GeneratedSpeechFormat.Flac; - if (StringComparer.OrdinalIgnoreCase.Equals(value, "wav")) return GeneratedSpeechFormat.Wav; - if (StringComparer.OrdinalIgnoreCase.Equals(value, "pcm")) return GeneratedSpeechFormat.Pcm; - throw new ArgumentOutOfRangeException(nameof(value), value, "Unknown GeneratedSpeechFormat value."); - } - } -} diff --git a/src/Generated/Models/GeneratedSpeechFormat.cs b/src/Generated/Models/GeneratedSpeechFormat.cs new file mode 100644 index 00000000..e8c256e3 --- /dev/null +++ b/src/Generated/Models/GeneratedSpeechFormat.cs @@ -0,0 +1,44 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; + +namespace OpenAI.Audio +{ + public readonly partial struct GeneratedSpeechFormat : IEquatable + { + private readonly string _value; + + public GeneratedSpeechFormat(string value) + { + _value = value ?? throw new ArgumentNullException(nameof(value)); + } + + private const string Mp3Value = "mp3"; + private const string OpusValue = "opus"; + private const string AacValue = "aac"; + private const string FlacValue = "flac"; + private const string WavValue = "wav"; + private const string PcmValue = "pcm"; + + public static GeneratedSpeechFormat Mp3 { get; } = new GeneratedSpeechFormat(Mp3Value); + public static GeneratedSpeechFormat Opus { get; } = new GeneratedSpeechFormat(OpusValue); + public static GeneratedSpeechFormat Aac { get; } = new GeneratedSpeechFormat(AacValue); + public static GeneratedSpeechFormat Flac { get; } = new GeneratedSpeechFormat(FlacValue); + public static GeneratedSpeechFormat Wav { get; } = new GeneratedSpeechFormat(WavValue); + public static GeneratedSpeechFormat Pcm { get; } = new GeneratedSpeechFormat(PcmValue); + public static bool operator ==(GeneratedSpeechFormat left, GeneratedSpeechFormat right) => left.Equals(right); + public static bool operator !=(GeneratedSpeechFormat left, GeneratedSpeechFormat right) => !left.Equals(right); + public static implicit operator GeneratedSpeechFormat(string value) => new GeneratedSpeechFormat(value); + + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is GeneratedSpeechFormat other && Equals(other); + public bool Equals(GeneratedSpeechFormat other) => string.Equals(_value, other._value, StringComparison.InvariantCultureIgnoreCase); + + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => _value != null ? StringComparer.InvariantCultureIgnoreCase.GetHashCode(_value) : 0; + public override string ToString() => _value; + } +} diff --git a/src/Generated/Models/InternalBatchRequestOutputResponse.Serialization.cs b/src/Generated/Models/InternalBatchRequestOutputResponse.Serialization.cs index 54280475..5f48d809 100644 --- a/src/Generated/Models/InternalBatchRequestOutputResponse.Serialization.cs +++ b/src/Generated/Models/InternalBatchRequestOutputResponse.Serialization.cs @@ -38,7 +38,19 @@ void IJsonModel.Write(Utf8JsonWriter writer, foreach (var item in Body) { writer.WritePropertyName(item.Key); - writer.WriteStringValue(item.Value); + if (item.Value == null) + { + writer.WriteNullValue(); + continue; + } +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (JsonDocument document = JsonDocument.Parse(item.Value)) + { + JsonSerializer.Serialize(writer, document.RootElement); + } +#endif } writer.WriteEndObject(); } @@ -86,7 +98,7 @@ internal static InternalBatchRequestOutputResponse DeserializeInternalBatchReque } int? statusCode = default; string requestId = default; - IReadOnlyDictionary body = default; + IReadOnlyDictionary body = default; IDictionary serializedAdditionalRawData = default; Dictionary rawDataDictionary = new Dictionary(); foreach (var property in element.EnumerateObject()) @@ -111,10 +123,17 @@ internal static InternalBatchRequestOutputResponse DeserializeInternalBatchReque { continue; } - Dictionary dictionary = new Dictionary(); + Dictionary dictionary = new Dictionary(); foreach (var property0 in property.Value.EnumerateObject()) { - dictionary.Add(property0.Name, property0.Value.GetString()); + if (property0.Value.ValueKind == JsonValueKind.Null) + { + dictionary.Add(property0.Name, null); + } + else + { + dictionary.Add(property0.Name, BinaryData.FromString(property0.Value.GetRawText())); + } } body = dictionary; continue; @@ -126,7 +145,7 @@ internal static InternalBatchRequestOutputResponse DeserializeInternalBatchReque } } serializedAdditionalRawData = rawDataDictionary; - return new InternalBatchRequestOutputResponse(statusCode, requestId, body ?? new ChangeTrackingDictionary(), serializedAdditionalRawData); + return new InternalBatchRequestOutputResponse(statusCode, requestId, body ?? new ChangeTrackingDictionary(), serializedAdditionalRawData); } BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) diff --git a/src/Generated/Models/InternalBatchRequestOutputResponse.cs b/src/Generated/Models/InternalBatchRequestOutputResponse.cs index 2919fec2..8435f20d 100644 --- a/src/Generated/Models/InternalBatchRequestOutputResponse.cs +++ b/src/Generated/Models/InternalBatchRequestOutputResponse.cs @@ -12,10 +12,10 @@ internal partial class InternalBatchRequestOutputResponse internal IDictionary SerializedAdditionalRawData { get; set; } internal InternalBatchRequestOutputResponse() { - Body = new ChangeTrackingDictionary(); + Body = new ChangeTrackingDictionary(); } - internal InternalBatchRequestOutputResponse(int? statusCode, string requestId, IReadOnlyDictionary body, IDictionary serializedAdditionalRawData) + internal InternalBatchRequestOutputResponse(int? statusCode, string requestId, IReadOnlyDictionary body, IDictionary serializedAdditionalRawData) { StatusCode = statusCode; RequestId = requestId; @@ -25,6 +25,6 @@ internal InternalBatchRequestOutputResponse(int? statusCode, string requestId, I public int? StatusCode { get; } public string RequestId { get; } - public IReadOnlyDictionary Body { get; } + public IReadOnlyDictionary Body { get; } } } diff --git a/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.Serialization.cs b/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.Serialization.cs index e9dd186c..78a34b22 100644 --- a/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.Serialization.cs +++ b/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.Serialization.cs @@ -38,7 +38,19 @@ void IJsonModel.Write( foreach (var item in FileSearch) { writer.WritePropertyName(item.Key); - writer.WriteStringValue(item.Value); + if (item.Value == null) + { + writer.WriteNullValue(); + continue; + } +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (JsonDocument document = JsonDocument.Parse(item.Value)) + { + JsonSerializer.Serialize(writer, document.RootElement); + } +#endif } writer.WriteEndObject(); } @@ -91,7 +103,7 @@ internal static InternalRunStepDeltaStepDetailsToolCallsFileSearchObject Deseria } int index = default; string id = default; - IReadOnlyDictionary fileSearch = default; + IReadOnlyDictionary fileSearch = default; string type = default; IDictionary serializedAdditionalRawData = default; Dictionary rawDataDictionary = new Dictionary(); @@ -109,10 +121,17 @@ internal static InternalRunStepDeltaStepDetailsToolCallsFileSearchObject Deseria } if (property.NameEquals("file_search"u8)) { - Dictionary dictionary = new Dictionary(); + Dictionary dictionary = new Dictionary(); foreach (var property0 in property.Value.EnumerateObject()) { - dictionary.Add(property0.Name, property0.Value.GetString()); + if (property0.Value.ValueKind == JsonValueKind.Null) + { + dictionary.Add(property0.Name, null); + } + else + { + dictionary.Add(property0.Name, BinaryData.FromString(property0.Value.GetRawText())); + } } fileSearch = dictionary; continue; diff --git a/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.cs b/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.cs index 6fcfc013..e3db470b 100644 --- a/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.cs +++ b/src/Generated/Models/InternalRunStepDeltaStepDetailsToolCallsFileSearchObject.cs @@ -9,7 +9,7 @@ namespace OpenAI.Assistants { internal partial class InternalRunStepDeltaStepDetailsToolCallsFileSearchObject : InternalRunStepDeltaStepDetailsToolCallsObjectToolCallsObject { - internal InternalRunStepDeltaStepDetailsToolCallsFileSearchObject(int index, IReadOnlyDictionary fileSearch) + internal InternalRunStepDeltaStepDetailsToolCallsFileSearchObject(int index, IReadOnlyDictionary fileSearch) { Argument.AssertNotNull(fileSearch, nameof(fileSearch)); @@ -18,7 +18,7 @@ internal InternalRunStepDeltaStepDetailsToolCallsFileSearchObject(int index, IRe FileSearch = fileSearch; } - internal InternalRunStepDeltaStepDetailsToolCallsFileSearchObject(string type, IDictionary serializedAdditionalRawData, int index, string id, IReadOnlyDictionary fileSearch) : base(type, serializedAdditionalRawData) + internal InternalRunStepDeltaStepDetailsToolCallsFileSearchObject(string type, IDictionary serializedAdditionalRawData, int index, string id, IReadOnlyDictionary fileSearch) : base(type, serializedAdditionalRawData) { Index = index; Id = id; @@ -31,6 +31,6 @@ internal InternalRunStepDeltaStepDetailsToolCallsFileSearchObject() public int Index { get; } public string Id { get; } - public IReadOnlyDictionary FileSearch { get; } + public IReadOnlyDictionary FileSearch { get; } } } diff --git a/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.Serialization.cs b/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.Serialization.cs index e89d929f..34d26af6 100644 --- a/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.Serialization.cs +++ b/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.Serialization.cs @@ -33,7 +33,19 @@ void IJsonModel.Write(Utf8JsonWriter w foreach (var item in FileSearch) { writer.WritePropertyName(item.Key); - writer.WriteStringValue(item.Value); + if (item.Value == null) + { + writer.WriteNullValue(); + continue; + } +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (JsonDocument document = JsonDocument.Parse(item.Value)) + { + JsonSerializer.Serialize(writer, document.RootElement); + } +#endif } writer.WriteEndObject(); } @@ -85,7 +97,7 @@ internal static InternalRunStepFileSearchToolCallDetails DeserializeInternalRunS return null; } string id = default; - IReadOnlyDictionary fileSearch = default; + IReadOnlyDictionary fileSearch = default; string type = default; IDictionary serializedAdditionalRawData = default; Dictionary rawDataDictionary = new Dictionary(); @@ -98,10 +110,17 @@ internal static InternalRunStepFileSearchToolCallDetails DeserializeInternalRunS } if (property.NameEquals("file_search"u8)) { - Dictionary dictionary = new Dictionary(); + Dictionary dictionary = new Dictionary(); foreach (var property0 in property.Value.EnumerateObject()) { - dictionary.Add(property0.Name, property0.Value.GetString()); + if (property0.Value.ValueKind == JsonValueKind.Null) + { + dictionary.Add(property0.Name, null); + } + else + { + dictionary.Add(property0.Name, BinaryData.FromString(property0.Value.GetRawText())); + } } fileSearch = dictionary; continue; diff --git a/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.cs b/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.cs index 6be9fec0..f1593afd 100644 --- a/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.cs +++ b/src/Generated/Models/InternalRunStepFileSearchToolCallDetails.cs @@ -9,7 +9,7 @@ namespace OpenAI.Assistants { internal partial class InternalRunStepFileSearchToolCallDetails : RunStepToolCall { - internal InternalRunStepFileSearchToolCallDetails(string id, IReadOnlyDictionary fileSearch) + internal InternalRunStepFileSearchToolCallDetails(string id, IReadOnlyDictionary fileSearch) { Argument.AssertNotNull(id, nameof(id)); Argument.AssertNotNull(fileSearch, nameof(fileSearch)); @@ -19,7 +19,7 @@ internal InternalRunStepFileSearchToolCallDetails(string id, IReadOnlyDictionary FileSearch = fileSearch; } - internal InternalRunStepFileSearchToolCallDetails(string type, IDictionary serializedAdditionalRawData, string id, IReadOnlyDictionary fileSearch) : base(type, serializedAdditionalRawData) + internal InternalRunStepFileSearchToolCallDetails(string type, IDictionary serializedAdditionalRawData, string id, IReadOnlyDictionary fileSearch) : base(type, serializedAdditionalRawData) { Id = id; FileSearch = fileSearch; @@ -30,6 +30,6 @@ internal InternalRunStepFileSearchToolCallDetails() } public string Id { get; } - public IReadOnlyDictionary FileSearch { get; } + public IReadOnlyDictionary FileSearch { get; } } } diff --git a/src/Generated/Models/SpeechGenerationOptions.Serialization.cs b/src/Generated/Models/SpeechGenerationOptions.Serialization.cs index 9b2c260b..823b6165 100644 --- a/src/Generated/Models/SpeechGenerationOptions.Serialization.cs +++ b/src/Generated/Models/SpeechGenerationOptions.Serialization.cs @@ -39,12 +39,12 @@ void IJsonModel.Write(Utf8JsonWriter writer, ModelReade if (SerializedAdditionalRawData?.ContainsKey("response_format") != true && Optional.IsDefined(ResponseFormat)) { writer.WritePropertyName("response_format"u8); - writer.WriteStringValue(ResponseFormat.Value.ToSerialString()); + writer.WriteStringValue(ResponseFormat.Value.ToString()); } - if (SerializedAdditionalRawData?.ContainsKey("speed") != true && Optional.IsDefined(Speed)) + if (SerializedAdditionalRawData?.ContainsKey("speed") != true && Optional.IsDefined(SpeedRatio)) { writer.WritePropertyName("speed"u8); - writer.WriteNumberValue(Speed.Value); + writer.WriteNumberValue(SpeedRatio.Value); } if (SerializedAdditionalRawData != null) { @@ -118,7 +118,7 @@ internal static SpeechGenerationOptions DeserializeSpeechGenerationOptions(JsonE { continue; } - responseFormat = property.Value.GetString().ToGeneratedSpeechFormat(); + responseFormat = new GeneratedSpeechFormat(property.Value.GetString()); continue; } if (property.NameEquals("speed"u8)) diff --git a/src/Generated/Models/SpeechGenerationOptions.cs b/src/Generated/Models/SpeechGenerationOptions.cs index 4a5b48c1..8ecda5dc 100644 --- a/src/Generated/Models/SpeechGenerationOptions.cs +++ b/src/Generated/Models/SpeechGenerationOptions.cs @@ -11,16 +11,15 @@ public partial class SpeechGenerationOptions { internal IDictionary SerializedAdditionalRawData { get; set; } - internal SpeechGenerationOptions(InternalCreateSpeechRequestModel model, string input, GeneratedSpeechVoice voice, GeneratedSpeechFormat? responseFormat, float? speed, IDictionary serializedAdditionalRawData) + internal SpeechGenerationOptions(InternalCreateSpeechRequestModel model, string input, GeneratedSpeechVoice voice, GeneratedSpeechFormat? responseFormat, float? speedRatio, IDictionary serializedAdditionalRawData) { Model = model; Input = input; Voice = voice; ResponseFormat = responseFormat; - Speed = speed; + SpeedRatio = speedRatio; SerializedAdditionalRawData = serializedAdditionalRawData; } public GeneratedSpeechFormat? ResponseFormat { get; set; } - public float? Speed { get; set; } } } diff --git a/tests/Assistants/AssistantTests.cs b/tests/Assistants/AssistantTests.cs index 898c4299..921d436a 100644 --- a/tests/Assistants/AssistantTests.cs +++ b/tests/Assistants/AssistantTests.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using OpenAI.Assistants; -using OpenAI.Chat; using OpenAI.Files; using OpenAI.VectorStores; using System; @@ -9,7 +8,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Numerics; using System.Threading; using System.Threading.Tasks; using static OpenAI.Tests.TestHelpers; @@ -679,6 +677,67 @@ This file describes the favorite foods of several people. Assert.That(hasCake, Is.True); } + [Test] + public async Task BasicFileSearchStreamingWorks() + { + const string fileContent = """ + The favorite food of several people: + - Summanus Ferdinand: tacos + - Tekakwitha Effie: pizza + - Filip Carola: cake + """; + + const string fileName = "favorite_foods.txt"; + + FileClient fileClient = GetTestClient(TestScenario.Files); + AssistantClient client = GetTestClient(TestScenario.Assistants); + + // First, upload a simple test file. + OpenAIFileInfo testFile = fileClient.UploadFile(BinaryData.FromString(fileContent), fileName, FileUploadPurpose.Assistants); + Validate(testFile); + + // Create an assistant, using the creation helper to make a new vector store. + AssistantCreationOptions assistantCreationOptions = new() + { + Tools = { new FileSearchToolDefinition() }, + ToolResources = new() + { + FileSearch = new() + { + NewVectorStores = { new VectorStoreCreationHelper([testFile.Id]) } + } + } + }; + Assistant assistant = client.CreateAssistant("gpt-4o-mini", assistantCreationOptions); + Validate(assistant); + + Assert.That(assistant.ToolResources?.FileSearch?.VectorStoreIds, Has.Count.EqualTo(1)); + string vectorStoreId = assistant.ToolResources.FileSearch.VectorStoreIds[0]; + _vectorStoreIdsToDelete.Add(vectorStoreId); + + // Create a thread. + ThreadCreationOptions threadCreationOptions = new() + { + InitialMessages = { "Using the files you have available, what's Filip's favorite food?" } + }; + AssistantThread thread = client.CreateThread(threadCreationOptions); + Validate(thread); + + // Create run and stream the results. + AsyncCollectionResult streamingResult = client.CreateRunStreamingAsync(thread.Id, assistant.Id); + string message = string.Empty; + + await foreach (StreamingUpdate update in streamingResult) + { + if (update is MessageContentUpdate contentUpdate) + { + message += $"{contentUpdate.Text}"; + } + } + + Assert.That(message, Does.Contain("cake")); + } + [Test] public async Task Pagination_CanEnumerateAssistants() { diff --git a/tests/Audio/TextToSpeechTests.cs b/tests/Audio/TextToSpeechTests.cs index cbbd748d..e875c69b 100644 --- a/tests/Audio/TextToSpeechTests.cs +++ b/tests/Audio/TextToSpeechTests.cs @@ -32,19 +32,31 @@ public async Task BasicTextToSpeechWorks() [Test] [TestCase(null)] - [TestCase(GeneratedSpeechFormat.Mp3)] - [TestCase(GeneratedSpeechFormat.Opus)] - [TestCase(GeneratedSpeechFormat.Aac)] - [TestCase(GeneratedSpeechFormat.Flac)] - [TestCase(GeneratedSpeechFormat.Wav)] - [TestCase(GeneratedSpeechFormat.Pcm)] - public async Task OutputFormatWorks(GeneratedSpeechFormat? responseFormat) + [TestCase("mp3")] + [TestCase("opus")] + [TestCase("aac")] + [TestCase("flac")] + [TestCase("wav")] + [TestCase("pcm")] + public async Task OutputFormatWorks(string responseFormat) { AudioClient client = GetTestClient(TestScenario.Audio_TTS); - SpeechGenerationOptions options = responseFormat == null - ? new() - : new() { ResponseFormat = responseFormat }; + SpeechGenerationOptions options = new(); + + if (!string.IsNullOrEmpty(responseFormat)) + { + options.ResponseFormat = responseFormat switch + { + "mp3" => GeneratedSpeechFormat.Mp3, + "opus" => GeneratedSpeechFormat.Opus, + "aac" => GeneratedSpeechFormat.Aac, + "flac" => GeneratedSpeechFormat.Flac, + "wav" => GeneratedSpeechFormat.Wav, + "pcm" => GeneratedSpeechFormat.Pcm, + _ => throw new ArgumentException("Invalid response format") + }; + } BinaryData audio = IsAsync ? await client.GenerateSpeechAsync("Hello, world!", GeneratedSpeechVoice.Alloy, options)