diff --git a/api/OpenAI.net8.0.cs b/api/OpenAI.net8.0.cs index 2d3ee43a0..7f20c9160 100644 --- a/api/OpenAI.net8.0.cs +++ b/api/OpenAI.net8.0.cs @@ -1439,6 +1439,14 @@ public class ChatClient { [Experimental("OPENAI001")] public virtual Task> GetChatCompletionAsync(string completionId, CancellationToken cancellationToken = default); [Experimental("OPENAI001")] + public virtual CollectionResult GetChatCompletionMessages(string completionId, ChatCompletionCollectionOptions options = null, CancellationToken cancellationToken = default); + [Experimental("OPENAI001")] + public virtual CollectionResult GetChatCompletionMessages(string completionId, string after, int? limit, string order, RequestOptions options); + [Experimental("OPENAI001")] + public virtual AsyncCollectionResult GetChatCompletionMessagesAsync(string completionId, ChatCompletionMessageCollectionOptions options = null, CancellationToken cancellationToken = default); + [Experimental("OPENAI001")] + public virtual AsyncCollectionResult GetChatCompletionMessagesAsync(string completionId, string after, int? limit, string order, RequestOptions options); + [Experimental("OPENAI001")] public virtual CollectionResult GetChatCompletions(ChatCompletionCollectionOptions options = null, CancellationToken cancellationToken = default); [Experimental("OPENAI001")] public virtual CollectionResult GetChatCompletions(string after, int? limit, string order, IDictionary metadata, string model, RequestOptions options); @@ -1511,6 +1519,29 @@ public class ChatCompletionDeletionResult : IJsonModel, IPersistableModel { + public string AfterId { get; set; } + public ChatCompletionCollectionOrder? Order { get; set; } + public int? PageSizeLimit { get; set; } + protected virtual ChatCompletionMessageCollectionOptions JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options); + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options); + protected virtual ChatCompletionMessageCollectionOptions PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options); + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options); + } + [Experimental("OPENAI001")] + public class ChatCompletionMessageListDatum : IJsonModel, IPersistableModel { + public IList Annotations { get; } + public ChatOutputAudio Audio { get; } + public string Content { get; } + public string Id { get; } + public string Refusal { get; } + public IReadOnlyList ToolCalls { get; } + protected virtual ChatCompletionMessageListDatum JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options); + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options); + protected virtual ChatCompletionMessageListDatum PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options); + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options); + } public class ChatCompletionOptions : IJsonModel, IPersistableModel { public bool? AllowParallelToolCalls { get; set; } [Experimental("OPENAI001")] diff --git a/api/OpenAI.netstandard2.0.cs b/api/OpenAI.netstandard2.0.cs index 8f24a5b95..4c4a518d8 100644 --- a/api/OpenAI.netstandard2.0.cs +++ b/api/OpenAI.netstandard2.0.cs @@ -1289,6 +1289,10 @@ public class ChatClient { public virtual ClientResult GetChatCompletion(string completionId, CancellationToken cancellationToken = default); public virtual Task GetChatCompletionAsync(string completionId, RequestOptions options); public virtual Task> GetChatCompletionAsync(string completionId, CancellationToken cancellationToken = default); + public virtual CollectionResult GetChatCompletionMessages(string completionId, ChatCompletionCollectionOptions options = null, CancellationToken cancellationToken = default); + public virtual CollectionResult GetChatCompletionMessages(string completionId, string after, int? limit, string order, RequestOptions options); + public virtual AsyncCollectionResult GetChatCompletionMessagesAsync(string completionId, ChatCompletionMessageCollectionOptions options = null, CancellationToken cancellationToken = default); + public virtual AsyncCollectionResult GetChatCompletionMessagesAsync(string completionId, string after, int? limit, string order, RequestOptions options); public virtual CollectionResult GetChatCompletions(ChatCompletionCollectionOptions options = null, CancellationToken cancellationToken = default); public virtual CollectionResult GetChatCompletions(string after, int? limit, string order, IDictionary metadata, string model, RequestOptions options); public virtual AsyncCollectionResult GetChatCompletionsAsync(ChatCompletionCollectionOptions options = null, CancellationToken cancellationToken = default); @@ -1349,6 +1353,27 @@ public class ChatCompletionDeletionResult : IJsonModel, IPersistableModel { + public string AfterId { get; set; } + public ChatCompletionCollectionOrder? Order { get; set; } + public int? PageSizeLimit { get; set; } + protected virtual ChatCompletionMessageCollectionOptions JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options); + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options); + protected virtual ChatCompletionMessageCollectionOptions PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options); + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options); + } + public class ChatCompletionMessageListDatum : IJsonModel, IPersistableModel { + public IList Annotations { get; } + public ChatOutputAudio Audio { get; } + public string Content { get; } + public string Id { get; } + public string Refusal { get; } + public IReadOnlyList ToolCalls { get; } + protected virtual ChatCompletionMessageListDatum JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options); + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options); + protected virtual ChatCompletionMessageListDatum PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options); + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options); + } public class ChatCompletionOptions : IJsonModel, IPersistableModel { public bool? AllowParallelToolCalls { get; set; } public ChatAudioOptions AudioOptions { get; set; } diff --git a/codegen/generator/src/Visitors/PaginationVisitor.cs b/codegen/generator/src/Visitors/PaginationVisitor.cs index c921f8a65..213bb8ffc 100644 --- a/codegen/generator/src/Visitors/PaginationVisitor.cs +++ b/codegen/generator/src/Visitors/PaginationVisitor.cs @@ -12,7 +12,7 @@ namespace OpenAILibraryPlugin.Visitors; /// -/// This visitor modifies GetRawPagesAsync methods to consider HasMore in addition to LastId when deciding whether to continue pagination. +/// This visitor modifies GetRawPagesAsync and GetRawPages methods to consider HasMore in addition to LastId when deciding whether to continue pagination. /// It also replaces specific parameters with an options type for pagination methods. /// public class PaginationVisitor : ScmLibraryVisitor @@ -37,6 +37,14 @@ public class PaginationVisitor : ScmLibraryVisitor { "GetChatCompletionsAsync", ("ChatCompletion", "ChatCompletionCollectionOptions", _chatParamsToReplace) + }, + { + "GetChatCompletionMessages", + ("ChatCompletionMessageListDatum", "ChatCompletionCollectionOptions", _chatParamsToReplace) + }, + { + "GetChatCompletionMessagesAsync", + ("ChatCompletionMessageListDatum", "ChatCompletionMessageCollectionOptions", _chatParamsToReplace) } }; @@ -174,10 +182,11 @@ nullConditional.Inner is VariableExpression varExpr2 && /// True if the method was handled, false otherwise. private bool TryHandleGetRawPagesAsyncMethod(MethodProvider method) { - // If the method is GetRawPagesAsync and is internal, we will modify the body statements to add a check for hasMore == false. + // If the method is GetRawPagesAsync or GetRawPages and is internal, we will modify the body statements to add a check for hasMore == false. // This is to ensure that pagination stops when hasMore is false, in addition to checking LastId. - if (method.Signature.Name == "GetRawPagesAsync" && method.EnclosingType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) + if ((method.Signature.Name == "GetRawPagesAsync" || method.Signature.Name == "GetRawPages") && method.EnclosingType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal)) { + VariableExpression? hasMoreVariable = null; var statements = method.BodyStatements?.ToList() ?? new List(); VisitExplodedMethodBodyStatements( statements!, @@ -198,14 +207,12 @@ private bool TryHandleGetRawPagesAsyncMethod(MethodProvider method) binaryExpr.Right is KeywordExpression rightKeyword && rightKeyword.Keyword == "null") { - // Create "hasMore == null" condition - var hasMoreNullCheck = new BinaryOperatorExpression( - "==", - new MemberExpression(null, "hasMore"), - Snippet.False); + // Create "!hasMore" condition. Note the hasMoreVariable gets assigned earlier in the method statements + // in the WhileStatement handler below. + var hasMoreNullCheck = Snippet.Not(hasMoreVariable); - // Return "nextToken == null || hasMore == null" - return new BinaryOperatorExpression("||", binaryExpr, hasMoreNullCheck); + // Return "nextToken == null || !hasMore" + return BoolSnippets.Or(binaryExpr.As(), hasMoreNullCheck); } } return expression; @@ -230,7 +237,7 @@ assignmentExpression.Value is MemberExpression memberExpression && { // Create a new assignment for hasMore var hasMoreAssignment = new AssignmentExpression( - new DeclarationExpression(typeof(bool), "hasMore"), + Snippet.Declare("hasMore", typeof(bool), out hasMoreVariable), new MemberExpression(memberExpression.Inner, "HasMore")); // Insert the new assignment before the existing one diff --git a/specification/client/chat.client.tsp b/specification/client/chat.client.tsp index 0613867fb..87f6251eb 100644 --- a/specification/client/chat.client.tsp +++ b/specification/client/chat.client.tsp @@ -14,6 +14,7 @@ using Azure.ClientGenerator.Core; @@usage(CreateChatCompletionStreamResponse, Usage.output); @@visibility(ChatCompletionResponseMessage.tool_calls, Lifecycle.Read); +@@visibility(ChatCompletionResponseMessage.annotations, Lifecycle.Read); @@visibility(ChatCompletionStreamResponseDelta.tool_calls, Lifecycle.Read); @@clientName(Chat.createChatCompletion, "CompleteChat"); diff --git a/specification/client/models/chat.models.tsp b/specification/client/models/chat.models.tsp index 8f239b501..79c699b3b 100644 --- a/specification/client/models/chat.models.tsp +++ b/specification/client/models/chat.models.tsp @@ -31,4 +31,26 @@ model ChatCompletionCollectionOptions { @query `model`?: string, } +alias ChatCompletionMessageCollectionOrderQueryParameter = { + /** + * Sort order by the `created_at` timestamp of the objects. `asc` for ascending order and`desc` + * for descending order. + */ + @query order?: ChatCompletionMessageCollectionOrder; +}; + +union ChatCompletionMessageCollectionOrder { + string, + Ascending: "asc", + Descending: "desc", +} + +@access(Access.public) +@usage(Usage.input) +model ChatCompletionMessageCollectionOptions { + ...CollectionAfterQueryParameter, + ...CollectionLimitQueryParameter, + ...ChatCompletionMessageCollectionOrderQueryParameter, +} + diff --git a/src/Custom/Chat/ChatClient.Protocol.cs b/src/Custom/Chat/ChatClient.Protocol.cs index 30d6ab2ac..b5d8e5d72 100644 --- a/src/Custom/Chat/ChatClient.Protocol.cs +++ b/src/Custom/Chat/ChatClient.Protocol.cs @@ -7,8 +7,6 @@ namespace OpenAI.Chat; /// The service client for the OpenAI Chat Completions endpoint. -[CodeGenSuppress("GetChatCompletionMessagesAsync", typeof(string), typeof(string), typeof(int?), typeof(string), typeof(RequestOptions))] -[CodeGenSuppress("GetChatCompletionMessages", typeof(string), typeof(string), typeof(int?), typeof(string), typeof(RequestOptions))] [CodeGenSuppress("UpdateChatCompletionAsync", typeof(string), typeof(BinaryContent), typeof(RequestOptions))] [CodeGenSuppress("UpdateChatCompletion", typeof(string), typeof(BinaryContent), typeof(RequestOptions))] public partial class ChatClient diff --git a/src/Custom/Chat/ChatClient.cs b/src/Custom/Chat/ChatClient.cs index 4ea4275b8..c5f294c2f 100644 --- a/src/Custom/Chat/ChatClient.cs +++ b/src/Custom/Chat/ChatClient.cs @@ -20,8 +20,6 @@ namespace OpenAI.Chat; [CodeGenSuppress("ChatClient", typeof(ClientPipeline), typeof(Uri))] [CodeGenSuppress("CompleteChat", typeof(ChatCompletionOptions), typeof(CancellationToken))] [CodeGenSuppress("CompleteChatAsync", typeof(ChatCompletionOptions), typeof(CancellationToken))] -[CodeGenSuppress("GetChatCompletionMessages", typeof(string), typeof(string), typeof(int?), typeof(OpenAI.VectorStores.VectorStoreCollectionOrder?), typeof(CancellationToken))] -[CodeGenSuppress("GetChatCompletionMessagesAsync", typeof(string), typeof(string), typeof(int?), typeof(OpenAI.VectorStores.VectorStoreCollectionOrder?), typeof(CancellationToken))] [CodeGenSuppress("UpdateChatCompletion", typeof(string), typeof(IDictionary), typeof(CancellationToken))] [CodeGenSuppress("UpdateChatCompletionAsync", typeof(string), typeof(IDictionary), typeof(CancellationToken))] public partial class ChatClient diff --git a/src/Custom/Chat/ChatCompletionMessageCollectionOptions.cs b/src/Custom/Chat/ChatCompletionMessageCollectionOptions.cs new file mode 100644 index 000000000..5d9b20a46 --- /dev/null +++ b/src/Custom/Chat/ChatCompletionMessageCollectionOptions.cs @@ -0,0 +1,5 @@ +namespace OpenAI.Chat; + +// CUSTOM: Use the correct namespace. +[CodeGenType("ChatCompletionMessageCollectionOptions")] +public partial class ChatCompletionMessageCollectionOptions {} \ No newline at end of file diff --git a/src/Custom/Chat/ChatCompletionMessageCollectionOrder.cs b/src/Custom/Chat/ChatCompletionMessageCollectionOrder.cs new file mode 100644 index 000000000..89e281dac --- /dev/null +++ b/src/Custom/Chat/ChatCompletionMessageCollectionOrder.cs @@ -0,0 +1,5 @@ +namespace OpenAI.Chat; + +// CUSTOM: Use the correct namespace. +[CodeGenType("ChatCompletionMessageCollectionOrder")] +public readonly partial struct ChatCompletionMessageCollectionOrder {} \ No newline at end of file diff --git a/src/Custom/Chat/Internal/InternalChatCompletionMessageListDatum.cs b/src/Custom/Chat/ChatCompletionMessageListDatum.cs similarity index 60% rename from src/Custom/Chat/Internal/InternalChatCompletionMessageListDatum.cs rename to src/Custom/Chat/ChatCompletionMessageListDatum.cs index 6b63f4c5a..8b571d6a1 100644 --- a/src/Custom/Chat/Internal/InternalChatCompletionMessageListDatum.cs +++ b/src/Custom/Chat/ChatCompletionMessageListDatum.cs @@ -1,9 +1,13 @@ namespace OpenAI.Chat; [CodeGenType("ChatCompletionMessageListDatum")] -internal partial class InternalChatCompletionMessageListDatum +public partial class ChatCompletionMessageListDatum { // CUSTOM: Ensure enumerated value is used. [CodeGenMember("Role")] internal ChatMessageRole Role { get; set; } = ChatMessageRole.Assistant; + + // CUSTOM: Rename + [CodeGenMember("Audio")] + public ChatOutputAudio OutputAudio { get; } } \ No newline at end of file diff --git a/src/Generated/ChatClient.cs b/src/Generated/ChatClient.cs index 7dcf1b723..d4d4f5ca5 100644 --- a/src/Generated/ChatClient.cs +++ b/src/Generated/ChatClient.cs @@ -90,5 +90,61 @@ public virtual async Task CompleteChatAsync(BinaryContent content, using PipelineMessage message = CreateCompleteChatRequest(content, options); return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); } + + [Experimental("OPENAI001")] + public virtual CollectionResult GetChatCompletionMessages(string completionId, string after, int? limit, string order, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + return new ChatClientGetChatCompletionMessagesCollectionResult( + this, + completionId, + after, + limit, + order, + options); + } + + [Experimental("OPENAI001")] + public virtual AsyncCollectionResult GetChatCompletionMessagesAsync(string completionId, string after, int? limit, string order, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + return new ChatClientGetChatCompletionMessagesAsyncCollectionResult( + this, + completionId, + after, + limit, + order, + options); + } + + [Experimental("OPENAI001")] + public virtual CollectionResult GetChatCompletionMessages(string completionId, ChatCompletionCollectionOptions options = default, CancellationToken cancellationToken = default) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + return new ChatClientGetChatCompletionMessagesCollectionResultOfT( + this, + completionId, + options?.AfterId, + options?.PageSizeLimit, + options?.Order?.ToString(), + cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); + } + + [Experimental("OPENAI001")] + public virtual AsyncCollectionResult GetChatCompletionMessagesAsync(string completionId, ChatCompletionMessageCollectionOptions options = default, CancellationToken cancellationToken = default) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + return new ChatClientGetChatCompletionMessagesAsyncCollectionResultOfT( + this, + completionId, + options?.AfterId, + options?.PageSizeLimit, + options?.Order?.ToString(), + cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); + } } } diff --git a/src/Generated/ChatClientGetChatCompletionMessagesAsyncCollectionResult.cs b/src/Generated/ChatClientGetChatCompletionMessagesAsyncCollectionResult.cs new file mode 100644 index 000000000..a0e6fcc6b --- /dev/null +++ b/src/Generated/ChatClientGetChatCompletionMessagesAsyncCollectionResult.cs @@ -0,0 +1,68 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using OpenAI; + +namespace OpenAI.Chat +{ + internal partial class ChatClientGetChatCompletionMessagesAsyncCollectionResult : AsyncCollectionResult + { + private readonly ChatClient _client; + private readonly string _completionId; + private readonly string _after; + private readonly int? _limit; + private readonly string _order; + private readonly RequestOptions _options; + + public ChatClientGetChatCompletionMessagesAsyncCollectionResult(ChatClient client, string completionId, string after, int? limit, string order, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + _client = client; + _completionId = completionId; + _after = after; + _limit = limit; + _order = order; + _options = options; + } + + public override async IAsyncEnumerable GetRawPagesAsync() + { + PipelineMessage message = _client.CreateGetChatCompletionMessagesRequest(_completionId, _after, _limit, _order, _options); + string nextToken = null; + while (true) + { + ClientResult result = ClientResult.FromResponse(await _client.Pipeline.ProcessMessageAsync(message, _options).ConfigureAwait(false)); + yield return result; + + // Plugin customization: add hasMore assignment + bool hasMore = ((InternalChatCompletionMessageList)result).HasMore; + nextToken = ((InternalChatCompletionMessageList)result).LastId; + // Plugin customization: add hasMore == false check to pagination condition + if (nextToken == null || !hasMore) + { + yield break; + } + message = _client.CreateGetChatCompletionMessagesRequest(_completionId, nextToken, _limit, _order, _options); + } + } + + public override ContinuationToken GetContinuationToken(ClientResult page) + { + string nextPage = ((InternalChatCompletionMessageList)page).LastId; + if (nextPage != null) + { + return ContinuationToken.FromBytes(BinaryData.FromString(nextPage)); + } + else + { + return null; + } + } + } +} diff --git a/src/Generated/ChatClientGetChatCompletionMessagesAsyncCollectionResultOfT.cs b/src/Generated/ChatClientGetChatCompletionMessagesAsyncCollectionResultOfT.cs new file mode 100644 index 000000000..c52b4bbe2 --- /dev/null +++ b/src/Generated/ChatClientGetChatCompletionMessagesAsyncCollectionResultOfT.cs @@ -0,0 +1,78 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Threading.Tasks; +using OpenAI; + +namespace OpenAI.Chat +{ + internal partial class ChatClientGetChatCompletionMessagesAsyncCollectionResultOfT : AsyncCollectionResult + { + private readonly ChatClient _client; + private readonly string _completionId; + private readonly string _after; + private readonly int? _limit; + private readonly string _order; + private readonly RequestOptions _options; + + public ChatClientGetChatCompletionMessagesAsyncCollectionResultOfT(ChatClient client, string completionId, string after, int? limit, string order, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + _client = client; + _completionId = completionId; + _after = after; + _limit = limit; + _order = order; + _options = options; + } + + public override async IAsyncEnumerable GetRawPagesAsync() + { + PipelineMessage message = _client.CreateGetChatCompletionMessagesRequest(_completionId, _after, _limit, _order, _options); + string nextToken = null; + while (true) + { + ClientResult result = ClientResult.FromResponse(await _client.Pipeline.ProcessMessageAsync(message, _options).ConfigureAwait(false)); + yield return result; + + // Plugin customization: add hasMore assignment + bool hasMore = ((InternalChatCompletionMessageList)result).HasMore; + nextToken = ((InternalChatCompletionMessageList)result).LastId; + // Plugin customization: add hasMore == false check to pagination condition + if (nextToken == null || !hasMore) + { + yield break; + } + message = _client.CreateGetChatCompletionMessagesRequest(_completionId, nextToken, _limit, _order, _options); + } + } + + public override ContinuationToken GetContinuationToken(ClientResult page) + { + string nextPage = ((InternalChatCompletionMessageList)page).LastId; + if (nextPage != null) + { + return ContinuationToken.FromBytes(BinaryData.FromString(nextPage)); + } + else + { + return null; + } + } + + protected override async IAsyncEnumerable GetValuesFromPageAsync(ClientResult page) + { + foreach (ChatCompletionMessageListDatum item in ((InternalChatCompletionMessageList)page).Data) + { + yield return item; + await Task.Yield(); + } + } + } +} diff --git a/src/Generated/ChatClientGetChatCompletionMessagesCollectionResult.cs b/src/Generated/ChatClientGetChatCompletionMessagesCollectionResult.cs new file mode 100644 index 000000000..a692fa336 --- /dev/null +++ b/src/Generated/ChatClientGetChatCompletionMessagesCollectionResult.cs @@ -0,0 +1,68 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using OpenAI; + +namespace OpenAI.Chat +{ + internal partial class ChatClientGetChatCompletionMessagesCollectionResult : CollectionResult + { + private readonly ChatClient _client; + private readonly string _completionId; + private readonly string _after; + private readonly int? _limit; + private readonly string _order; + private readonly RequestOptions _options; + + public ChatClientGetChatCompletionMessagesCollectionResult(ChatClient client, string completionId, string after, int? limit, string order, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + _client = client; + _completionId = completionId; + _after = after; + _limit = limit; + _order = order; + _options = options; + } + + public override IEnumerable GetRawPages() + { + PipelineMessage message = _client.CreateGetChatCompletionMessagesRequest(_completionId, _after, _limit, _order, _options); + string nextToken = null; + while (true) + { + ClientResult result = ClientResult.FromResponse(_client.Pipeline.ProcessMessage(message, _options)); + yield return result; + + // Plugin customization: add hasMore assignment + bool hasMore = ((InternalChatCompletionMessageList)result).HasMore; + nextToken = ((InternalChatCompletionMessageList)result).LastId; + // Plugin customization: add hasMore == false check to pagination condition + if (nextToken == null || !hasMore) + { + yield break; + } + message = _client.CreateGetChatCompletionMessagesRequest(_completionId, nextToken, _limit, _order, _options); + } + } + + public override ContinuationToken GetContinuationToken(ClientResult page) + { + string nextPage = ((InternalChatCompletionMessageList)page).LastId; + if (nextPage != null) + { + return ContinuationToken.FromBytes(BinaryData.FromString(nextPage)); + } + else + { + return null; + } + } + } +} diff --git a/src/Generated/ChatClientGetChatCompletionMessagesCollectionResultOfT.cs b/src/Generated/ChatClientGetChatCompletionMessagesCollectionResultOfT.cs new file mode 100644 index 000000000..f7e3019d8 --- /dev/null +++ b/src/Generated/ChatClientGetChatCompletionMessagesCollectionResultOfT.cs @@ -0,0 +1,73 @@ +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using OpenAI; + +namespace OpenAI.Chat +{ + internal partial class ChatClientGetChatCompletionMessagesCollectionResultOfT : CollectionResult + { + private readonly ChatClient _client; + private readonly string _completionId; + private readonly string _after; + private readonly int? _limit; + private readonly string _order; + private readonly RequestOptions _options; + + public ChatClientGetChatCompletionMessagesCollectionResultOfT(ChatClient client, string completionId, string after, int? limit, string order, RequestOptions options) + { + Argument.AssertNotNullOrEmpty(completionId, nameof(completionId)); + + _client = client; + _completionId = completionId; + _after = after; + _limit = limit; + _order = order; + _options = options; + } + + public override IEnumerable GetRawPages() + { + PipelineMessage message = _client.CreateGetChatCompletionMessagesRequest(_completionId, _after, _limit, _order, _options); + string nextToken = null; + while (true) + { + ClientResult result = ClientResult.FromResponse(_client.Pipeline.ProcessMessage(message, _options)); + yield return result; + + // Plugin customization: add hasMore assignment + bool hasMore = ((InternalChatCompletionMessageList)result).HasMore; + nextToken = ((InternalChatCompletionMessageList)result).LastId; + // Plugin customization: add hasMore == false check to pagination condition + if (nextToken == null || !hasMore) + { + yield break; + } + message = _client.CreateGetChatCompletionMessagesRequest(_completionId, nextToken, _limit, _order, _options); + } + } + + public override ContinuationToken GetContinuationToken(ClientResult page) + { + string nextPage = ((InternalChatCompletionMessageList)page).LastId; + if (nextPage != null) + { + return ContinuationToken.FromBytes(BinaryData.FromString(nextPage)); + } + else + { + return null; + } + } + + protected override IEnumerable GetValuesFromPage(ClientResult page) + { + return ((InternalChatCompletionMessageList)page).Data; + } + } +} diff --git a/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResult.cs b/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResult.cs index 5740521e7..13b91e844 100644 --- a/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResult.cs +++ b/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResult.cs @@ -43,7 +43,7 @@ public override async IAsyncEnumerable GetRawPagesAsync() bool hasMore = ((InternalChatCompletionList)result).HasMore; nextToken = ((InternalChatCompletionList)result).LastId; // Plugin customization: add hasMore == false check to pagination condition - if (nextToken == null || hasMore == false) + if (nextToken == null || !hasMore) { yield break; } diff --git a/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResultOfT.cs b/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResultOfT.cs index d486452b1..453b7841e 100644 --- a/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResultOfT.cs +++ b/src/Generated/ChatClientGetChatCompletionsAsyncCollectionResultOfT.cs @@ -44,7 +44,7 @@ public override async IAsyncEnumerable GetRawPagesAsync() bool hasMore = ((InternalChatCompletionList)result).HasMore; nextToken = ((InternalChatCompletionList)result).LastId; // Plugin customization: add hasMore == false check to pagination condition - if (nextToken == null || hasMore == false) + if (nextToken == null || !hasMore) { yield break; } diff --git a/src/Generated/ChatClientGetChatCompletionsCollectionResult.cs b/src/Generated/ChatClientGetChatCompletionsCollectionResult.cs index f62c24cc9..19768b172 100644 --- a/src/Generated/ChatClientGetChatCompletionsCollectionResult.cs +++ b/src/Generated/ChatClientGetChatCompletionsCollectionResult.cs @@ -39,8 +39,11 @@ public override IEnumerable GetRawPages() ClientResult result = ClientResult.FromResponse(_client.Pipeline.ProcessMessage(message, _options)); yield return result; + // Plugin customization: add hasMore assignment + bool hasMore = ((InternalChatCompletionList)result).HasMore; nextToken = ((InternalChatCompletionList)result).LastId; - if (nextToken == null) + // Plugin customization: add hasMore == false check to pagination condition + if (nextToken == null || !hasMore) { yield break; } diff --git a/src/Generated/ChatClientGetChatCompletionsCollectionResultOfT.cs b/src/Generated/ChatClientGetChatCompletionsCollectionResultOfT.cs index 9e3ba0378..24ca282db 100644 --- a/src/Generated/ChatClientGetChatCompletionsCollectionResultOfT.cs +++ b/src/Generated/ChatClientGetChatCompletionsCollectionResultOfT.cs @@ -39,8 +39,11 @@ public override IEnumerable GetRawPages() ClientResult result = ClientResult.FromResponse(_client.Pipeline.ProcessMessage(message, _options)); yield return result; + // Plugin customization: add hasMore assignment + bool hasMore = ((InternalChatCompletionList)result).HasMore; nextToken = ((InternalChatCompletionList)result).LastId; - if (nextToken == null) + // Plugin customization: add hasMore == false check to pagination condition + if (nextToken == null || !hasMore) { yield break; } diff --git a/src/Generated/Models/Chat/ChatCompletionMessageCollectionOptions.Serialization.cs b/src/Generated/Models/Chat/ChatCompletionMessageCollectionOptions.Serialization.cs new file mode 100644 index 000000000..319e957f7 --- /dev/null +++ b/src/Generated/Models/Chat/ChatCompletionMessageCollectionOptions.Serialization.cs @@ -0,0 +1,115 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text.Json; +using OpenAI; + +namespace OpenAI.Chat +{ + public partial class ChatCompletionMessageCollectionOptions : IJsonModel + { + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(ChatCompletionMessageCollectionOptions)} does not support writing '{format}' format."); + } + // Plugin customization: remove options.Format != "W" check + if (_additionalBinaryDataProperties != null) + { + foreach (var item in _additionalBinaryDataProperties) + { + if (ModelSerializationExtensions.IsSentinelValue(item.Value)) + { + continue; + } + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (JsonDocument document = JsonDocument.Parse(item.Value)) + { + JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + } + } + + ChatCompletionMessageCollectionOptions IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => JsonModelCreateCore(ref reader, options); + + protected virtual ChatCompletionMessageCollectionOptions JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "J") + { + throw new FormatException($"The model {nameof(ChatCompletionMessageCollectionOptions)} does not support reading '{format}' format."); + } + using JsonDocument document = JsonDocument.ParseValue(ref reader); + return DeserializeChatCompletionMessageCollectionOptions(document.RootElement, options); + } + + internal static ChatCompletionMessageCollectionOptions DeserializeChatCompletionMessageCollectionOptions(JsonElement element, ModelReaderWriterOptions options) + { + if (element.ValueKind == JsonValueKind.Null) + { + return null; + } + string afterId = default; + int? pageSizeLimit = default; + ChatCompletionMessageCollectionOrder? order = default; + IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary(); + foreach (var prop in element.EnumerateObject()) + { + // Plugin customization: remove options.Format != "W" check + additionalBinaryDataProperties.Add(prop.Name, BinaryData.FromString(prop.Value.GetRawText())); + } + return new ChatCompletionMessageCollectionOptions(afterId, pageSizeLimit, order, additionalBinaryDataProperties); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + return ModelReaderWriter.Write(this, options, OpenAIContext.Default); + default: + throw new FormatException($"The model {nameof(ChatCompletionMessageCollectionOptions)} does not support writing '{options.Format}' format."); + } + } + + ChatCompletionMessageCollectionOptions IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); + + protected virtual ChatCompletionMessageCollectionOptions PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + using (JsonDocument document = JsonDocument.Parse(data)) + { + return DeserializeChatCompletionMessageCollectionOptions(document.RootElement, options); + } + default: + throw new FormatException($"The model {nameof(ChatCompletionMessageCollectionOptions)} does not support reading '{options.Format}' format."); + } + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; + } +} diff --git a/src/Generated/Models/Chat/ChatCompletionMessageCollectionOptions.cs b/src/Generated/Models/Chat/ChatCompletionMessageCollectionOptions.cs new file mode 100644 index 000000000..538fc57ac --- /dev/null +++ b/src/Generated/Models/Chat/ChatCompletionMessageCollectionOptions.cs @@ -0,0 +1,40 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace OpenAI.Chat +{ + [Experimental("OPENAI001")] + public partial class ChatCompletionMessageCollectionOptions + { + private protected IDictionary _additionalBinaryDataProperties; + + public ChatCompletionMessageCollectionOptions() + { + } + + internal ChatCompletionMessageCollectionOptions(string afterId, int? pageSizeLimit, ChatCompletionMessageCollectionOrder? order, IDictionary additionalBinaryDataProperties) + { + AfterId = afterId; + PageSizeLimit = pageSizeLimit; + Order = order; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public string AfterId { get; set; } + + public int? PageSizeLimit { get; set; } + + public ChatCompletionMessageCollectionOrder? Order { get; set; } + + internal IDictionary SerializedAdditionalRawData + { + get => _additionalBinaryDataProperties; + set => _additionalBinaryDataProperties = value; + } + } +} diff --git a/src/Generated/Models/Chat/ChatCompletionMessageCollectionOrder.cs b/src/Generated/Models/Chat/ChatCompletionMessageCollectionOrder.cs new file mode 100644 index 000000000..84dec8b0a --- /dev/null +++ b/src/Generated/Models/Chat/ChatCompletionMessageCollectionOrder.cs @@ -0,0 +1,46 @@ +// + +#nullable disable + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using OpenAI; + +namespace OpenAI.Chat +{ + [Experimental("OPENAI001")] + public readonly partial struct ChatCompletionMessageCollectionOrder : IEquatable + { + private readonly string _value; + private const string AscendingValue = "asc"; + private const string DescendingValue = "desc"; + + public ChatCompletionMessageCollectionOrder(string value) + { + Argument.AssertNotNull(value, nameof(value)); + + _value = value; + } + + public static ChatCompletionMessageCollectionOrder Ascending { get; } = new ChatCompletionMessageCollectionOrder(AscendingValue); + + public static ChatCompletionMessageCollectionOrder Descending { get; } = new ChatCompletionMessageCollectionOrder(DescendingValue); + + public static bool operator ==(ChatCompletionMessageCollectionOrder left, ChatCompletionMessageCollectionOrder right) => left.Equals(right); + + public static bool operator !=(ChatCompletionMessageCollectionOrder left, ChatCompletionMessageCollectionOrder right) => !left.Equals(right); + + public static implicit operator ChatCompletionMessageCollectionOrder(string value) => new ChatCompletionMessageCollectionOrder(value); + + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => obj is ChatCompletionMessageCollectionOrder other && Equals(other); + + public bool Equals(ChatCompletionMessageCollectionOrder 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/Chat/InternalChatCompletionMessageListDatum.Serialization.cs b/src/Generated/Models/Chat/ChatCompletionMessageListDatum.Serialization.cs similarity index 74% rename from src/Generated/Models/Chat/InternalChatCompletionMessageListDatum.Serialization.cs rename to src/Generated/Models/Chat/ChatCompletionMessageListDatum.Serialization.cs index 3290fffd8..501914eb8 100644 --- a/src/Generated/Models/Chat/InternalChatCompletionMessageListDatum.Serialization.cs +++ b/src/Generated/Models/Chat/ChatCompletionMessageListDatum.Serialization.cs @@ -10,13 +10,13 @@ namespace OpenAI.Chat { - internal partial class InternalChatCompletionMessageListDatum : IJsonModel + public partial class ChatCompletionMessageListDatum : IJsonModel { - internal InternalChatCompletionMessageListDatum() : this(null, null, null, null, null, null, null, default, null) + internal ChatCompletionMessageListDatum() : this(null, null, null, null, null, null, default, null, null) { } - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) { writer.WriteStartObject(); JsonModelWriteCore(writer, options); @@ -25,10 +25,10 @@ void IJsonModel.Write(Utf8JsonWriter wri protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; if (format != "J") { - throw new FormatException($"The model {nameof(InternalChatCompletionMessageListDatum)} does not support writing '{format}' format."); + throw new FormatException($"The model {nameof(ChatCompletionMessageListDatum)} does not support writing '{format}' format."); } if (_additionalBinaryDataProperties?.ContainsKey("content") != true) { @@ -65,6 +65,7 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit } writer.WriteEndArray(); } + // Plugin customization: remove options.Format != "W" check if (Optional.IsCollectionDefined(Annotations) && _additionalBinaryDataProperties?.ContainsKey("annotations") != true) { writer.WritePropertyName("annotations"u8); @@ -80,11 +81,6 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit writer.WritePropertyName("function_call"u8); writer.WriteObjectValue(FunctionCall, options); } - if (Optional.IsDefined(Audio) && _additionalBinaryDataProperties?.ContainsKey("audio") != true) - { - writer.WritePropertyName("audio"u8); - writer.WriteObjectValue(Audio, options); - } if (_additionalBinaryDataProperties?.ContainsKey("id") != true) { writer.WritePropertyName("id"u8); @@ -95,6 +91,11 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit writer.WritePropertyName("role"u8); writer.WriteStringValue(Role.ToSerialString()); } + if (Optional.IsDefined(OutputAudio) && _additionalBinaryDataProperties?.ContainsKey("audio") != true) + { + writer.WritePropertyName("audio"u8); + writer.WriteObjectValue(OutputAudio, options); + } // Plugin customization: remove options.Format != "W" check if (_additionalBinaryDataProperties != null) { @@ -117,20 +118,20 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit } } - InternalChatCompletionMessageListDatum IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => JsonModelCreateCore(ref reader, options); + ChatCompletionMessageListDatum IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) => JsonModelCreateCore(ref reader, options); - protected virtual InternalChatCompletionMessageListDatum JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + protected virtual ChatCompletionMessageListDatum JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; if (format != "J") { - throw new FormatException($"The model {nameof(InternalChatCompletionMessageListDatum)} does not support reading '{format}' format."); + throw new FormatException($"The model {nameof(ChatCompletionMessageListDatum)} does not support reading '{format}' format."); } using JsonDocument document = JsonDocument.ParseValue(ref reader); - return DeserializeInternalChatCompletionMessageListDatum(document.RootElement, options); + return DeserializeChatCompletionMessageListDatum(document.RootElement, options); } - internal static InternalChatCompletionMessageListDatum DeserializeInternalChatCompletionMessageListDatum(JsonElement element, ModelReaderWriterOptions options) + internal static ChatCompletionMessageListDatum DeserializeChatCompletionMessageListDatum(JsonElement element, ModelReaderWriterOptions options) { if (element.ValueKind == JsonValueKind.Null) { @@ -139,11 +140,11 @@ internal static InternalChatCompletionMessageListDatum DeserializeInternalChatCo string content = default; string refusal = default; IReadOnlyList toolCalls = default; - IList annotations = default; + IReadOnlyList annotations = default; InternalChatCompletionResponseMessageFunctionCall functionCall = default; - ChatOutputAudio audio = default; string id = default; ChatMessageRole role = default; + ChatOutputAudio outputAudio = default; IDictionary additionalBinaryDataProperties = new ChangeTrackingDictionary(); foreach (var prop in element.EnumerateObject()) { @@ -204,16 +205,6 @@ internal static InternalChatCompletionMessageListDatum DeserializeInternalChatCo functionCall = InternalChatCompletionResponseMessageFunctionCall.DeserializeInternalChatCompletionResponseMessageFunctionCall(prop.Value, options); continue; } - if (prop.NameEquals("audio"u8)) - { - if (prop.Value.ValueKind == JsonValueKind.Null) - { - audio = null; - continue; - } - audio = ChatOutputAudio.DeserializeChatOutputAudio(prop.Value, options); - continue; - } if (prop.NameEquals("id"u8)) { id = prop.Value.GetString(); @@ -224,52 +215,62 @@ internal static InternalChatCompletionMessageListDatum DeserializeInternalChatCo role = prop.Value.GetString().ToChatMessageRole(); continue; } + if (prop.NameEquals("audio"u8)) + { + if (prop.Value.ValueKind == JsonValueKind.Null) + { + outputAudio = null; + continue; + } + outputAudio = ChatOutputAudio.DeserializeChatOutputAudio(prop.Value, options); + continue; + } // Plugin customization: remove options.Format != "W" check additionalBinaryDataProperties.Add(prop.Name, BinaryData.FromString(prop.Value.GetRawText())); } - return new InternalChatCompletionMessageListDatum( + return new ChatCompletionMessageListDatum( content, refusal, toolCalls ?? new ChangeTrackingList(), annotations ?? new ChangeTrackingList(), functionCall, - audio, id, role, + outputAudio, additionalBinaryDataProperties); } - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; switch (format) { case "J": return ModelReaderWriter.Write(this, options, OpenAIContext.Default); default: - throw new FormatException($"The model {nameof(InternalChatCompletionMessageListDatum)} does not support writing '{options.Format}' format."); + throw new FormatException($"The model {nameof(ChatCompletionMessageListDatum)} does not support writing '{options.Format}' format."); } } - InternalChatCompletionMessageListDatum IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); + ChatCompletionMessageListDatum IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); - protected virtual InternalChatCompletionMessageListDatum PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) + protected virtual ChatCompletionMessageListDatum PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; switch (format) { case "J": using (JsonDocument document = JsonDocument.Parse(data)) { - return DeserializeInternalChatCompletionMessageListDatum(document.RootElement, options); + return DeserializeChatCompletionMessageListDatum(document.RootElement, options); } default: - throw new FormatException($"The model {nameof(InternalChatCompletionMessageListDatum)} does not support reading '{options.Format}' format."); + throw new FormatException($"The model {nameof(ChatCompletionMessageListDatum)} does not support reading '{options.Format}' format."); } } - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; } } diff --git a/src/Generated/Models/Chat/InternalChatCompletionMessageListDatum.cs b/src/Generated/Models/Chat/ChatCompletionMessageListDatum.cs similarity index 70% rename from src/Generated/Models/Chat/InternalChatCompletionMessageListDatum.cs rename to src/Generated/Models/Chat/ChatCompletionMessageListDatum.cs index ccf4e8d15..5320514f8 100644 --- a/src/Generated/Models/Chat/InternalChatCompletionMessageListDatum.cs +++ b/src/Generated/Models/Chat/ChatCompletionMessageListDatum.cs @@ -4,15 +4,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using OpenAI; namespace OpenAI.Chat { - internal partial class InternalChatCompletionMessageListDatum + [Experimental("OPENAI001")] + public partial class ChatCompletionMessageListDatum { private protected IDictionary _additionalBinaryDataProperties; - internal InternalChatCompletionMessageListDatum(string content, string refusal, string id, ChatMessageRole role) + internal ChatCompletionMessageListDatum(string content, string refusal, string id, ChatMessageRole role) { Content = content; Refusal = refusal; @@ -22,7 +24,7 @@ internal InternalChatCompletionMessageListDatum(string content, string refusal, Role = role; } - internal InternalChatCompletionMessageListDatum(string content, string refusal, IReadOnlyList toolCalls, IList annotations, InternalChatCompletionResponseMessageFunctionCall functionCall, ChatOutputAudio audio, string id, ChatMessageRole role, IDictionary additionalBinaryDataProperties) + internal ChatCompletionMessageListDatum(string content, string refusal, IReadOnlyList toolCalls, IReadOnlyList annotations, InternalChatCompletionResponseMessageFunctionCall functionCall, string id, ChatMessageRole role, ChatOutputAudio outputAudio, IDictionary additionalBinaryDataProperties) { // Plugin customization: ensure initialization of collections Content = content; @@ -30,9 +32,9 @@ internal InternalChatCompletionMessageListDatum(string content, string refusal, ToolCalls = toolCalls ?? new ChangeTrackingList(); Annotations = annotations ?? new ChangeTrackingList(); FunctionCall = functionCall; - Audio = audio; Id = id; Role = role; + OutputAudio = outputAudio; _additionalBinaryDataProperties = additionalBinaryDataProperties; } @@ -42,12 +44,10 @@ internal InternalChatCompletionMessageListDatum(string content, string refusal, public IReadOnlyList ToolCalls { get; } - public IList Annotations { get; } + public IReadOnlyList Annotations { get; } internal InternalChatCompletionResponseMessageFunctionCall FunctionCall { get; } - public ChatOutputAudio Audio { get; } - public string Id { get; } internal IDictionary SerializedAdditionalRawData diff --git a/src/Generated/Models/Chat/InternalChatCompletionMessageList.Serialization.cs b/src/Generated/Models/Chat/InternalChatCompletionMessageList.Serialization.cs index 4318e9032..0395681c5 100644 --- a/src/Generated/Models/Chat/InternalChatCompletionMessageList.Serialization.cs +++ b/src/Generated/Models/Chat/InternalChatCompletionMessageList.Serialization.cs @@ -40,7 +40,7 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit { writer.WritePropertyName("data"u8); writer.WriteStartArray(); - foreach (InternalChatCompletionMessageListDatum item in Data) + foreach (ChatCompletionMessageListDatum item in Data) { writer.WriteObjectValue(item, options); } @@ -103,7 +103,7 @@ internal static InternalChatCompletionMessageList DeserializeInternalChatComplet return null; } string @object = default; - IList data = default; + IList data = default; string firstId = default; string lastId = default; bool hasMore = default; @@ -117,10 +117,10 @@ internal static InternalChatCompletionMessageList DeserializeInternalChatComplet } if (prop.NameEquals("data"u8)) { - List array = new List(); + List array = new List(); foreach (var item in prop.Value.EnumerateArray()) { - array.Add(InternalChatCompletionMessageListDatum.DeserializeInternalChatCompletionMessageListDatum(item, options)); + array.Add(ChatCompletionMessageListDatum.DeserializeChatCompletionMessageListDatum(item, options)); } data = array; continue; diff --git a/src/Generated/Models/Chat/InternalChatCompletionMessageList.cs b/src/Generated/Models/Chat/InternalChatCompletionMessageList.cs index 9f836c305..4b57decd0 100644 --- a/src/Generated/Models/Chat/InternalChatCompletionMessageList.cs +++ b/src/Generated/Models/Chat/InternalChatCompletionMessageList.cs @@ -13,7 +13,7 @@ internal partial class InternalChatCompletionMessageList { private protected IDictionary _additionalBinaryDataProperties; - internal InternalChatCompletionMessageList(IEnumerable data, string firstId, string lastId, bool hasMore) + internal InternalChatCompletionMessageList(IEnumerable data, string firstId, string lastId, bool hasMore) { Data = data.ToList(); FirstId = firstId; @@ -21,11 +21,11 @@ internal InternalChatCompletionMessageList(IEnumerable data, string firstId, string lastId, bool hasMore, IDictionary additionalBinaryDataProperties) + internal InternalChatCompletionMessageList(string @object, IList data, string firstId, string lastId, bool hasMore, IDictionary additionalBinaryDataProperties) { // Plugin customization: ensure initialization of collections Object = @object; - Data = data ?? new ChangeTrackingList(); + Data = data ?? new ChangeTrackingList(); FirstId = firstId; LastId = lastId; HasMore = hasMore; @@ -34,7 +34,7 @@ internal InternalChatCompletionMessageList(string @object, IList Data { get; } + public IList Data { get; } public string FirstId { get; } diff --git a/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.Serialization.cs b/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.Serialization.cs index 6becf428f..e248c97ff 100644 --- a/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.Serialization.cs +++ b/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.Serialization.cs @@ -53,6 +53,7 @@ protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWrit } writer.WriteEndArray(); } + // Plugin customization: remove options.Format != "W" check if (Optional.IsCollectionDefined(Annotations) && _additionalBinaryDataProperties?.ContainsKey("annotations") != true) { writer.WritePropertyName("annotations"u8); @@ -133,7 +134,7 @@ internal static InternalChatCompletionResponseMessage DeserializeInternalChatCom } string refusal = default; IReadOnlyList toolCalls = default; - IList annotations = default; + IReadOnlyList annotations = default; ChatOutputAudio audio = default; ChatMessageRole role = default; ChatMessageContent content = default; diff --git a/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.cs b/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.cs index 463f78ea7..49c7ead27 100644 --- a/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.cs +++ b/src/Generated/Models/Chat/InternalChatCompletionResponseMessage.cs @@ -22,7 +22,7 @@ internal InternalChatCompletionResponseMessage(string refusal, ChatMessageRole r Content = content ?? new ChatMessageContent(); } - internal InternalChatCompletionResponseMessage(string refusal, IReadOnlyList toolCalls, IList annotations, ChatOutputAudio audio, ChatMessageRole role, ChatMessageContent content, ChatFunctionCall functionCall, IDictionary additionalBinaryDataProperties) + internal InternalChatCompletionResponseMessage(string refusal, IReadOnlyList toolCalls, IReadOnlyList annotations, ChatOutputAudio audio, ChatMessageRole role, ChatMessageContent content, ChatFunctionCall functionCall, IDictionary additionalBinaryDataProperties) { // Plugin customization: ensure initialization of collections Refusal = refusal; @@ -39,7 +39,7 @@ internal InternalChatCompletionResponseMessage(string refusal, IReadOnlyList ToolCalls { get; } - public IList Annotations { get; } + public IReadOnlyList Annotations { get; } public ChatOutputAudio Audio { get; } diff --git a/src/Generated/OpenAIModelFactory.cs b/src/Generated/OpenAIModelFactory.cs index 72c0f256b..3f3eaf18d 100644 --- a/src/Generated/OpenAIModelFactory.cs +++ b/src/Generated/OpenAIModelFactory.cs @@ -1083,6 +1083,11 @@ public static ChatCompletionCollectionOptions ChatCompletionCollectionOptions(st additionalBinaryDataProperties: null); } + public static ChatCompletionMessageCollectionOptions ChatCompletionMessageCollectionOptions(string afterId = default, int? pageSizeLimit = default, ChatCompletionMessageCollectionOrder? order = default) + { + return new ChatCompletionMessageCollectionOptions(afterId, pageSizeLimit, order, additionalBinaryDataProperties: null); + } + public static AudioTokenLogProbabilityDetails AudioTokenLogProbabilityDetails(string token = default, float logProbability = default, ReadOnlyMemory utf8Bytes = default) { return new AudioTokenLogProbabilityDetails(token, logProbability, utf8Bytes, additionalBinaryDataProperties: null); diff --git a/tests/Chat/ChatTests.cs b/tests/Chat/ChatTests.cs index 6b63c4fea..40ba407a8 100644 --- a/tests/Chat/ChatTests.cs +++ b/tests/Chat/ChatTests.cs @@ -221,7 +221,7 @@ public async Task StreamingChatCanBeCancelledAsync() Assert.That(firstUpdate, Is.Not.Null); Assert.That(cancellationTokenSource.IsCancellationRequested, Is.False); - Thread.Sleep(1000); + await Task.Delay(1000); Assert.ThrowsAsync(async () => { @@ -1090,7 +1090,7 @@ public async Task GetChatCompletionsWithPagination() completionIds.Add(completion.Id); } - Thread.Sleep(5000); // Wait for completions to be stored + await Task.Delay(5000); // Wait for completions to be stored // Test pagination with limit ChatCompletionCollectionOptions paginationOptions = new() @@ -1146,7 +1146,7 @@ public async Task GetChatCompletionsWithAfterIdPagination() completionIds.Add(completion.Id); } - Thread.Sleep(5000); // Wait for completions to be stored + await Task.Delay(5000); // Wait for completions to be stored // Get first completion to use as afterId string afterId = null; @@ -1205,10 +1205,10 @@ public async Task GetChatCompletionsWithOrderFiltering() createOptions); completionIds.Add(completion.Id); - Thread.Sleep(1000); // Ensure different timestamps + await Task.Delay(1000); // Ensure different timestamps } - Thread.Sleep(5000); // Wait for completions to be stored + await Task.Delay(5000); // Wait for completions to be stored // Test ascending order ChatCompletionCollectionOptions ascOptions = new() @@ -1290,7 +1290,7 @@ public async Task GetChatCompletionsWithMetadataFiltering() options2); completionIds.Add(otherCompletion.Id); - Thread.Sleep(5000); // Wait for completions to be stored + await Task.Delay(5000); // Wait for completions to be stored // Filter by specific metadata ChatCompletionCollectionOptions filterOptions = new() @@ -1338,7 +1338,7 @@ public async Task GetChatCompletionsWithModelFiltering() ["Model filter test: Say 'Hello'"], createOptions); - Thread.Sleep(5000); // Wait for completion to be stored + await Task.Delay(5000); // Wait for completion to be stored // Filter by the model used by the test client ChatCompletionCollectionOptions filterOptions = new() @@ -1381,7 +1381,7 @@ public async Task GetChatCompletionsWithEmptyOptions() ["Empty options test: Say 'Hello'"], createOptions); - Thread.Sleep(5000); // Wait for completion to be stored + await Task.Delay(5000); // Wait for completion to be stored // Test with default/empty options int count = 0; @@ -1413,8 +1413,8 @@ public async Task GetChatCompletionsWithCombinedFilters() ChatCompletionOptions createOptions = new() { StoredOutputEnabled = true, - Metadata = - { + Metadata = + { [testKey] = "combined_value", ["test_type"] = "integration" } @@ -1424,7 +1424,7 @@ public async Task GetChatCompletionsWithCombinedFilters() ["Combined filters test: Say 'Combined test'"], createOptions); - Thread.Sleep(5000); // Wait for completion to be stored + await Task.Delay(6000); // Wait for completion to be stored // Test with combined filters ChatCompletionCollectionOptions combinedOptions = new() @@ -1440,7 +1440,7 @@ public async Task GetChatCompletionsWithCombinedFilters() { count++; Assert.That(fetchedCompletion.Id, Is.Not.Null.And.Not.Empty); - + if (count >= 10) break; // Prevent excessive iterations } @@ -1522,18 +1522,20 @@ public async Task StoredChatCompletionsWork() [new UserChatMessage("Say `this is a test`.")], options); - Thread.Sleep(5000); + await RetryWithExponentialBackoffAsync(async () => + { - ChatCompletion storedCompletion = await client.GetChatCompletionAsync(completion.Id); + ChatCompletion storedCompletion = await client.GetChatCompletionAsync(completion.Id); - Assert.That(storedCompletion.Id, Is.EqualTo(completion.Id)); - Assert.That(storedCompletion.Content[0].Text, Is.EqualTo(completion.Content[0].Text)); + Assert.That(storedCompletion.Id, Is.EqualTo(completion.Id)); + Assert.That(storedCompletion.Content[0].Text, Is.EqualTo(completion.Content[0].Text)); - ChatCompletionDeletionResult deletionResult = await client.DeleteChatCompletionAsync(completion.Id); + ChatCompletionDeletionResult deletionResult = await client.DeleteChatCompletionAsync(completion.Id); - Assert.That(deletionResult.Deleted, Is.True); + Assert.That(deletionResult.Deleted, Is.True); + }); - Thread.Sleep(5000); + await Task.Delay(5000); Assert.ThrowsAsync(async () => { @@ -1571,7 +1573,7 @@ public async Task GetChatCompletionsValidatesCollectionEnumeration() ["Enumeration test: Say 'Test enumeration'"], createOptions); - Thread.Sleep(5000); // Wait for completion to be stored + await Task.Delay(5000); // Wait for completion to be stored // Test that we can enumerate multiple times ChatCompletionCollectionOptions collectionOptions = new() @@ -1625,7 +1627,7 @@ public async Task GetChatCompletionsHandlesLargeLimits() ["Large limit test: Say 'Testing large limits'"], createOptions); - Thread.Sleep(5000); // Wait for completion to be stored + await Task.Delay(5000); // Wait for completion to be stored // Test with a large page size limit ChatCompletionCollectionOptions largeOptions = new() @@ -1666,7 +1668,7 @@ public async Task GetChatCompletionsWithMinimalLimits() ["Minimal limit test: Say 'Testing minimal limits'"], createOptions); - Thread.Sleep(5000); // Wait for completion to be stored + await Task.Delay(5000); // Wait for completion to be stored // Test with minimal page size ChatCompletionCollectionOptions minimalOptions = new() @@ -1692,6 +1694,386 @@ public async Task GetChatCompletionsWithMinimalLimits() catch { /* Ignore cleanup errors */ } } + [Test] + public async Task GetChatCompletionMessagesWithBasicUsage() + { + ChatClient client = GetTestClient(); + + // Create a completion with stored output enabled to have messages + ChatCompletionOptions createOptions = new() + { + StoredOutputEnabled = true, + Metadata = { ["test_scenario"] = "basic_messages" } + }; + + ChatCompletion completion = await client.CompleteChatAsync( + ["Basic messages test: Say 'Hello, this is a test message.'"], + createOptions); + + await RetryWithExponentialBackoffAsync(async () => + { + // Test basic enumeration of messages + int messageCount = 0; + await foreach (var message in client.GetChatCompletionMessagesAsync(completion.Id)) + { + messageCount++; + Assert.That(message.Id, Is.Not.Null.And.Not.Empty); + Assert.AreEqual("Basic messages test: Say 'Hello, this is a test message.'", message.Content); + + if (messageCount >= 5) break; // Prevent infinite loop + } + + Assert.That(messageCount, Is.GreaterThan(0)); + }); + + // Clean up + try + { + await client.DeleteChatCompletionAsync(completion.Id); + } + catch { /* Ignore cleanup errors */ } + } + + [Test] + public async Task GetChatCompletionMessagesWithPagination() + { + ChatClient client = GetTestClient(); + + // Create completion with multiple messages (conversation with tool calls) + List conversationMessages = new() + { + new UserChatMessage("What's the weather like today? Use the weather tool."), + new UserChatMessage("Name something I could do outside in this weather."), + new UserChatMessage("Name something else I could do outside in this weather."), + new UserChatMessage("Name something yet another thing I could do outside in this weather.") + }; + + // Add function definition to trigger more back-and-forth + ChatTool weatherTool = ChatTool.CreateFunctionTool( + "get_weather", + "Get current weather information", + BinaryData.FromString(""" + { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": ["location"] + } + """)); + + ChatCompletionOptions createOptions = new() + { + StoredOutputEnabled = true, + Tools = { weatherTool }, + Metadata = { ["test_scenario"] = "pagination_messages" } + }; + + ChatCompletion completion = await client.CompleteChatAsync( + conversationMessages, + createOptions); + + await RetryWithExponentialBackoffAsync(async () => + { + // Test pagination with limit + int totalMessages = 0; + string lastMessageId = null; + + var options = new ChatCompletionMessageCollectionOptions() + { + PageSizeLimit = 2 + }; + + await foreach (var message in client.GetChatCompletionMessagesAsync(completion.Id, options)) + { + totalMessages++; + lastMessageId = message.Id; + Assert.That(message.Id, Is.Not.Null.And.Not.Empty); + + if (totalMessages >= 4) break; // Get a few pages worth + } + + Assert.That(totalMessages, Is.GreaterThan(3)); + Assert.That(lastMessageId, Is.Not.Null); + }); + + // Clean up + try + { + await client.DeleteChatCompletionAsync(completion.Id); + } + catch { /* Ignore cleanup errors */ } + } + + [Test] + public async Task GetChatCompletionMessagesWithAfterIdPagination() + { + ChatClient client = GetTestClient(); + + // Create completion + ChatCompletionOptions createOptions = new() + { + StoredOutputEnabled = true, + Metadata = { ["test_scenario"] = "after_id_pagination" } + }; + + ChatCompletion completion = await client.CompleteChatAsync( + ["After ID pagination test: Please provide a detailed response with multiple sentences."], + createOptions); + + await RetryWithExponentialBackoffAsync(async () => + { + // Get first message to use as afterId + string afterId = null; + await foreach (var firstMessage in client.GetChatCompletionMessagesAsync(completion.Id)) + { + afterId = firstMessage.Id; + break; + } + + if (afterId != null) + { + // Test pagination starting after the first message + int count = 0; + var options = new ChatCompletionMessageCollectionOptions() + { + AfterId = afterId, + PageSizeLimit = 3 + }; + + await foreach (var message in client.GetChatCompletionMessagesAsync(completion.Id, options)) + { + count++; + // Ensure we don't get the afterId message + Assert.That(message.Id, Is.Not.EqualTo(afterId)); + + if (count >= 3) break; + } + + // We might not have messages after the first one, so just verify the method works + Assert.That(count, Is.GreaterThanOrEqualTo(0)); + } + }); + // Clean up + try + { + await client.DeleteChatCompletionAsync(completion.Id); + } + catch { /* Ignore cleanup errors */ } + } + + [Test] + public async Task GetChatCompletionMessagesWithOrderFiltering() + { + ChatClient client = GetTestClient(); + + // Create completion with detailed conversation + ChatCompletionOptions createOptions = new() + { + StoredOutputEnabled = true, + Metadata = { ["test_scenario"] = "order_filtering" } + }; + + ChatCompletion completion = await client.CompleteChatAsync( + ["Order filtering test: Please provide a comprehensive response about machine learning."], + createOptions); + + await RetryWithExponentialBackoffAsync(async () => + { + // Test ascending order + List ascMessages = new(); + var ascOptions = new ChatCompletionMessageCollectionOptions() + { + Order = ChatCompletionMessageCollectionOrder.Ascending, + PageSizeLimit = 5 + }; + + await foreach (var message in client.GetChatCompletionMessagesAsync(completion.Id, ascOptions)) + { + ascMessages.Add(message); + if (ascMessages.Count >= 3) break; + } + + // Test descending order + List descMessages = new(); + var descOptions = new ChatCompletionMessageCollectionOptions() + { + Order = ChatCompletionMessageCollectionOrder.Descending, + PageSizeLimit = 5 + }; + + await foreach (var message in client.GetChatCompletionMessagesAsync(completion.Id, descOptions)) + { + descMessages.Add(message); + if (descMessages.Count >= 3) break; + } + + // Verify we get results in both cases + Assert.That(ascMessages, Has.Count.GreaterThan(0)); + Assert.That(descMessages, Has.Count.GreaterThan(0)); + }); + + // Clean up + try + { + await client.DeleteChatCompletionAsync(completion.Id); + } + catch { /* Ignore cleanup errors */ } + } + + [Test] + public async Task GetChatCompletionMessagesWithCancellationToken() + { + ChatClient client = GetTestClient(); + + // Create completion + ChatCompletionOptions createOptions = new() + { + StoredOutputEnabled = true, + Metadata = { ["test_scenario"] = "cancellation_token" } + }; + + ChatCompletion completion = await client.CompleteChatAsync( + ["Cancellation test: Say 'Hello World'"], + createOptions); + + // Test with cancellation token + using var cts = new CancellationTokenSource(); + + await RetryWithExponentialBackoffAsync(async () => + { + try + { + int count = 0; + await foreach (var message in client.GetChatCompletionMessagesAsync(completion.Id, cancellationToken: cts.Token)) + { + count++; + Assert.That(message.Id, Is.Not.Null.And.Not.Empty); + + if (count >= 2) + { + cts.Cancel(); // Cancel after getting some messages + break; + } + } + + Assert.That(count, Is.GreaterThanOrEqualTo(1)); + } + catch (OperationCanceledException) + { + // This is expected if cancellation happens during enumeration + } + }); + + // Clean up + try + { + await client.DeleteChatCompletionAsync(completion.Id); + } + catch { /* Ignore cleanup errors */ } + } + + [Test] + public async Task GetChatCompletionMessagesWithCombinedOptions() + { + ChatClient client = GetTestClient(); + + // Create completion with comprehensive options + ChatCompletionOptions createOptions = new() + { + StoredOutputEnabled = true, + Metadata = { ["test_scenario"] = "combined_options" } + }; + + ChatCompletion completion = await client.CompleteChatAsync( + ["Combined options test: Provide a detailed explanation of artificial intelligence."], + createOptions); + + await RetryWithExponentialBackoffAsync(async () => + { + // Test combined options: limit + order + cancellation token + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + List messages = new(); + + await foreach (var message in client.GetChatCompletionMessagesAsync( + completion.Id, + new ChatCompletionMessageCollectionOptions() + { + PageSizeLimit = 3, + Order = ChatCompletionMessageCollectionOrder.Descending + }, + cancellationToken: cts.Token)) + { + messages.Add(message); + + // Validate message structure + Assert.That(message.Id, Is.Not.Null.And.Not.Empty); + + if (messages.Count >= 3) break; + } + + Assert.That(messages, Has.Count.GreaterThan(0)); + }); + + // Clean up + try + { + await client.DeleteChatCompletionAsync(completion.Id); + } + catch { /* Ignore cleanup errors */ } + } + + [Test] + public async Task GetChatCompletionMessagesHandlesNonExistentCompletion() + { + ChatClient client = GetTestClient(); + + // Test with non-existent completion ID + string nonExistentId = "comp_nonexistent_12345"; + + try + { + await foreach (var message in client.GetChatCompletionMessagesAsync(nonExistentId)) + { + Assert.Fail("Should not enumerate messages for non-existent completion"); + } + } + catch (ClientResultException ex) + { + // Should get some kind of error (likely ClientResultException or similar) + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Status, Is.EqualTo(404)); + } + } + + [Test] + public void GetChatCompletionMessagesWithInvalidParameters() + { + ChatClient client = GetTestClient(); + + // Test with null completion ID + Assert.ThrowsAsync(async () => + { + await foreach (var message in client.GetChatCompletionMessagesAsync(null)) + { + // Should not reach here + } + }); + + // Test with empty completion ID + Assert.ThrowsAsync(async () => + { + await foreach (var message in client.GetChatCompletionMessagesAsync("")) + { + // Should not reach here + } + }); + } + [OneTimeTearDown] public void TearDown() { @@ -1705,5 +2087,32 @@ public void TearDown() } } + private static async Task RetryWithExponentialBackoffAsync(Func action, int maxRetries = 5, int initialWaitMs = 750) + { + int waitDuration = initialWaitMs; + int retryCount = 0; + bool successful = false; + + while (retryCount < maxRetries && !successful) + { + try + { + await action(); + successful = true; + } + catch (ClientResultException ex) when (ex.Status == 404) + { + // If we get a 404, it means the resource is not yet available + await Task.Delay(waitDuration); + waitDuration *= 2; // Exponential backoff + retryCount++; + if (retryCount >= maxRetries) + { + throw; // Re-throw the exception if we've exhausted all retries + } + } + } + } + private static ChatClient GetTestClient(string overrideModel = null) => GetTestClient(TestScenario.Chat, overrideModel); }