From 36b009ed8cc6c197f77c7a11808ec521fffda32f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 15 Sep 2025 22:38:18 -0400 Subject: [PATCH 1/3] Add OpenAIResponseClient.CreateResponse{Streaming}Async overloads that take RequestOptions Today, if you're using the convenience types and you just want to set a user-agent header on a request (you're handed the instance and thus can't configure it at creation), the only supported option I see is to abandon the convenience methods and adopt the protocol methods, which means formatting all the input JSON and parsing all the response SSE / JSON. That's a big pill to swallow. Based on need, could we add a few convenience overloads that just take a RequestOptions instead of a CancellationToken? It results in minimal additional surface area / almost no additional duplicated code, as it's just taking a RequestOptions instead of taking a CancellationToken and calling ToRequestOptions on it. I've demonstrated in this PR with CreateResponseAsync and CreateResponseStreamingAsync, which are the two methods I currently care about. --- src/Custom/Responses/OpenAIResponseClient.cs | 30 +++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Custom/Responses/OpenAIResponseClient.cs b/src/Custom/Responses/OpenAIResponseClient.cs index 8096d1cda..174dae58b 100644 --- a/src/Custom/Responses/OpenAIResponseClient.cs +++ b/src/Custom/Responses/OpenAIResponseClient.cs @@ -119,12 +119,23 @@ protected internal OpenAIResponseClient(ClientPipeline pipeline, string model, O [Experimental("OPENAI001")] public Uri Endpoint => _endpoint; - public virtual async Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default) + public virtual Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default) + { + return CreateResponseAsync(inputItems, options, cancellationToken.ToRequestOptions()); + } + + public virtual async Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options, RequestOptions requestOptions) { Argument.AssertNotNullOrEmpty(inputItems, nameof(inputItems)); + Argument.AssertNotNull(requestOptions, nameof(requestOptions)); + + if (requestOptions.BufferResponse is false) + { + throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'true' when calling 'CreateResponseAsync'."); + } using BinaryContent content = CreatePerCallOptions(options, inputItems, stream: false).ToBinaryContent(); - ClientResult protocolResult = await CreateResponseAsync(content, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + ClientResult protocolResult = await CreateResponseAsync(content, requestOptions).ConfigureAwait(false); OpenAIResponse convenienceValue = (OpenAIResponse)protocolResult; return ClientResult.FromValue(convenienceValue, protocolResult.GetRawResponse()); } @@ -161,14 +172,25 @@ public virtual ClientResult CreateResponse(string userInputText, } public virtual AsyncCollectionResult CreateResponseStreamingAsync(IEnumerable inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default) + { + return CreateResponseStreamingAsync(inputItems, options, cancellationToken.ToRequestOptions(streaming: true)); + } + + public virtual AsyncCollectionResult CreateResponseStreamingAsync(IEnumerable inputItems, ResponseCreationOptions options, RequestOptions requestOptions) { Argument.AssertNotNullOrEmpty(inputItems, nameof(inputItems)); + Argument.AssertNotNull(requestOptions, nameof(requestOptions)); + + if (requestOptions.BufferResponse is true) + { + throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'false' when calling 'CreateResponseStreamingAsync'."); + } using BinaryContent content = CreatePerCallOptions(options, inputItems, stream: true).ToBinaryContent(); return new AsyncSseUpdateCollection( - async () => await CreateResponseAsync(content, cancellationToken.ToRequestOptions(streaming: true)).ConfigureAwait(false), + async () => await CreateResponseAsync(content, requestOptions).ConfigureAwait(false), StreamingResponseUpdate.DeserializeStreamingResponseUpdate, - cancellationToken); + requestOptions.CancellationToken); } public virtual CollectionResult CreateResponseStreaming(IEnumerable inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default) From 9d3a79871af1d6ab060afec6abe69a12e1cdc79d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 17 Sep 2025 09:26:52 -0400 Subject: [PATCH 2/3] Make new overloads internal --- src/Custom/Chat/ChatClient.cs | 28 +++++++++++++++++--- src/Custom/Responses/OpenAIResponseClient.cs | 6 ++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Custom/Chat/ChatClient.cs b/src/Custom/Chat/ChatClient.cs index 533990629..1f877d2e2 100644 --- a/src/Custom/Chat/ChatClient.cs +++ b/src/Custom/Chat/ChatClient.cs @@ -136,9 +136,19 @@ protected internal ChatClient(ClientPipeline pipeline, string model, OpenAIClien /// A token that can be used to cancel this method call. /// is null. /// is an empty collection, and was expected to be non-empty. - public virtual async Task> CompleteChatAsync(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) + public virtual Task> CompleteChatAsync(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) + { + return CompleteChatAsync(messages, options, cancellationToken.ToRequestOptions()); + } + + internal async Task> CompleteChatAsync(IEnumerable messages, ChatCompletionOptions options, RequestOptions requestOptions) { Argument.AssertNotNullOrEmpty(messages, nameof(messages)); + Argument.AssertNotNull(requestOptions, nameof(requestOptions)); + if (requestOptions.BufferResponse is false) + { + throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'true' when calling 'CompleteChatAsync'."); + } options ??= new(); CreateChatCompletionOptions(messages, ref options); @@ -148,7 +158,7 @@ public virtual async Task> CompleteChatAsync(IEnume { using BinaryContent content = options.ToBinaryContent(); - ClientResult result = await CompleteChatAsync(content, cancellationToken.ToRequestOptions()).ConfigureAwait(false); + ClientResult result = await CompleteChatAsync(content, requestOptions).ConfigureAwait(false); ChatCompletion chatCompletion = (ChatCompletion)result; scope?.RecordChatCompletion(chatCompletion); return ClientResult.FromValue(chatCompletion, result.GetRawResponse()); @@ -218,17 +228,27 @@ public virtual ClientResult CompleteChat(params ChatMessage[] me /// is null. /// is an empty collection, and was expected to be non-empty. public virtual AsyncCollectionResult CompleteChatStreamingAsync(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) + { + return CompleteChatStreamingAsync(messages, options, cancellationToken.ToRequestOptions(streaming: true)); + } + + internal AsyncCollectionResult CompleteChatStreamingAsync(IEnumerable messages, ChatCompletionOptions options, RequestOptions requestOptions) { Argument.AssertNotNull(messages, nameof(messages)); + Argument.AssertNotNull(requestOptions, nameof(requestOptions)); + if (requestOptions.BufferResponse is true) + { + throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'false' when calling 'CompleteChatStreamingAsync'."); + } options ??= new(); CreateChatCompletionOptions(messages, ref options, stream: true); using BinaryContent content = options.ToBinaryContent(); return new AsyncSseUpdateCollection( - async () => await CompleteChatAsync(content, cancellationToken.ToRequestOptions(streaming: true)).ConfigureAwait(false), + async () => await CompleteChatAsync(content, requestOptions).ConfigureAwait(false), StreamingChatCompletionUpdate.DeserializeStreamingChatCompletionUpdate, - cancellationToken); + requestOptions.CancellationToken); } /// diff --git a/src/Custom/Responses/OpenAIResponseClient.cs b/src/Custom/Responses/OpenAIResponseClient.cs index 174dae58b..14d15a75e 100644 --- a/src/Custom/Responses/OpenAIResponseClient.cs +++ b/src/Custom/Responses/OpenAIResponseClient.cs @@ -124,11 +124,10 @@ public virtual Task> CreateResponseAsync(IEnumerabl return CreateResponseAsync(inputItems, options, cancellationToken.ToRequestOptions()); } - public virtual async Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options, RequestOptions requestOptions) + internal async Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options, RequestOptions requestOptions) { Argument.AssertNotNullOrEmpty(inputItems, nameof(inputItems)); Argument.AssertNotNull(requestOptions, nameof(requestOptions)); - if (requestOptions.BufferResponse is false) { throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'true' when calling 'CreateResponseAsync'."); @@ -176,11 +175,10 @@ public virtual AsyncCollectionResult CreateResponseStre return CreateResponseStreamingAsync(inputItems, options, cancellationToken.ToRequestOptions(streaming: true)); } - public virtual AsyncCollectionResult CreateResponseStreamingAsync(IEnumerable inputItems, ResponseCreationOptions options, RequestOptions requestOptions) + internal AsyncCollectionResult CreateResponseStreamingAsync(IEnumerable inputItems, ResponseCreationOptions options, RequestOptions requestOptions) { Argument.AssertNotNullOrEmpty(inputItems, nameof(inputItems)); Argument.AssertNotNull(requestOptions, nameof(requestOptions)); - if (requestOptions.BufferResponse is true) { throw new InvalidOperationException("'requestOptions.BufferResponse' must be 'false' when calling 'CreateResponseStreamingAsync'."); From d3cf6dbc5e600099a952303c8dac7c4cbeb28cfa Mon Sep 17 00:00:00 2001 From: Jose Arriaga Maldonado Date: Wed, 17 Sep 2025 16:37:20 -0700 Subject: [PATCH 3/3] Fix for null RequestOptions --- src/Custom/Chat/ChatClient.cs | 2 +- src/Custom/Responses/OpenAIResponseClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Custom/Chat/ChatClient.cs b/src/Custom/Chat/ChatClient.cs index 1f877d2e2..db85b601d 100644 --- a/src/Custom/Chat/ChatClient.cs +++ b/src/Custom/Chat/ChatClient.cs @@ -138,7 +138,7 @@ protected internal ChatClient(ClientPipeline pipeline, string model, OpenAIClien /// is an empty collection, and was expected to be non-empty. public virtual Task> CompleteChatAsync(IEnumerable messages, ChatCompletionOptions options = null, CancellationToken cancellationToken = default) { - return CompleteChatAsync(messages, options, cancellationToken.ToRequestOptions()); + return CompleteChatAsync(messages, options, cancellationToken.ToRequestOptions() ?? new RequestOptions()); } internal async Task> CompleteChatAsync(IEnumerable messages, ChatCompletionOptions options, RequestOptions requestOptions) diff --git a/src/Custom/Responses/OpenAIResponseClient.cs b/src/Custom/Responses/OpenAIResponseClient.cs index 14d15a75e..aebbdc588 100644 --- a/src/Custom/Responses/OpenAIResponseClient.cs +++ b/src/Custom/Responses/OpenAIResponseClient.cs @@ -121,7 +121,7 @@ protected internal OpenAIResponseClient(ClientPipeline pipeline, string model, O public virtual Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default) { - return CreateResponseAsync(inputItems, options, cancellationToken.ToRequestOptions()); + return CreateResponseAsync(inputItems, options, cancellationToken.ToRequestOptions() ?? new RequestOptions()); } internal async Task> CreateResponseAsync(IEnumerable inputItems, ResponseCreationOptions options, RequestOptions requestOptions)