From 6a4e4d0d8a490ec1deada317b44758be45ea6e9c Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Wed, 25 Mar 2026 18:58:41 +0000
Subject: [PATCH 1/2] Add AsIChatClientWithStoredOutputDisabled for
ProjectResponsesClient
Add extension method on ProjectResponsesClient in Microsoft.Agents.AI.AzureAI
package (Azure.AI.Extensions.OpenAI namespace) mirroring the existing extension
on ResponsesClient in the OpenAI package. This enables Azure AI consumers to
disable server-side response storage without depending on the OpenAI package.
- New ProjectResponsesClientExtensions class with AsIChatClientWithStoredOutputDisabled
- Optional deploymentName parameter (model is no longer required)
- Updated OpenAI counterpart doc to remove 'Required' wording for model param
- Added unit tests covering null guard, inner client accessibility,
StoredOutputEnabled=false, and reasoning encrypted content inclusion/exclusion
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../ProjectResponsesClientExtensions.cs | 45 +++++
.../OpenAIResponseClientExtensions.cs | 2 +-
.../ProjectResponsesClientExtensionsTests.cs | 169 ++++++++++++++++++
3 files changed, 215 insertions(+), 1 deletion(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/ProjectResponsesClientExtensionsTests.cs
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs
new file mode 100644
index 0000000000..25869ca1eb
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.DiagnosticIds;
+using Microsoft.Shared.Diagnostics;
+using OpenAI.Responses;
+
+namespace Azure.AI.Extensions.OpenAI;
+
+///
+/// Provides extension methods for
+/// to simplify the creation of AI agents that work with Azure AI services.
+///
+[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
+public static class ProjectResponsesClientExtensions
+{
+ ///
+ /// Gets an for use with this that does not store responses for later retrieval.
+ ///
+ ///
+ /// This corresponds to setting the "store" property in the JSON representation to false.
+ ///
+ /// The client.
+ /// Optional deployment name (model) to use for requests.
+ ///
+ /// Includes an encrypted version of reasoning tokens in reasoning item outputs.
+ /// This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly
+ /// (like when the store parameter is set to false, or when an organization is enrolled in the zero data retention program).
+ /// Defaults to .
+ ///
+ /// An that can be used to converse via the that does not store responses for later retrieval.
+ /// is .
+ [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+ public static IChatClient AsIChatClientWithStoredOutputDisabled(this ProjectResponsesClient responseClient, string? deploymentName = null, bool includeReasoningEncryptedContent = true)
+ {
+ return Throw.IfNull(responseClient)
+ .AsIChatClient(deploymentName)
+ .AsBuilder()
+ .ConfigureOptions(x => x.RawRepresentationFactory = _ => includeReasoningEncryptedContent
+ ? new CreateResponseOptions() { StoredOutputEnabled = false, IncludedProperties = { IncludedResponseProperty.ReasoningEncryptedContent } }
+ : new CreateResponseOptions() { StoredOutputEnabled = false })
+ .Build();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs
index 5aee8eb046..4ceff75743 100644
--- a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIResponseClientExtensions.cs
@@ -105,7 +105,7 @@ public static ChatClientAgent AsAIAgent(
/// This corresponds to setting the "store" property in the JSON representation to false.
///
/// The client.
- /// Optional default model ID to use for requests. Required when using a plain (not via Azure OpenAI).
+ /// Optional default model ID to use for requests.
///
/// Includes an encrypted version of reasoning tokens in reasoning item outputs.
/// This enables reasoning items to be used in multi-turn conversations when using the Responses API statelessly
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/ProjectResponsesClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/ProjectResponsesClientExtensionsTests.cs
new file mode 100644
index 0000000000..d10ef861c9
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/ProjectResponsesClientExtensionsTests.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Reflection;
+using Azure.AI.Extensions.OpenAI;
+using Microsoft.Extensions.AI;
+using OpenAI.Responses;
+
+namespace Microsoft.Agents.AI.AzureAI.UnitTests;
+
+///
+/// Unit tests for the class.
+///
+public sealed class ProjectResponsesClientExtensionsTests
+{
+ private static ProjectResponsesClient CreateTestClient()
+ {
+ return new ProjectResponsesClient(new FakeAuthenticationTokenProvider());
+ }
+
+ ///
+ /// Verify that AsIChatClientWithStoredOutputDisabled throws ArgumentNullException when client is null.
+ ///
+ [Fact]
+ public void AsIChatClientWithStoredOutputDisabled_WithNullClient_ThrowsArgumentNullException()
+ {
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ ((ProjectResponsesClient)null!).AsIChatClientWithStoredOutputDisabled());
+
+ Assert.Equal("responseClient", exception.ParamName);
+ }
+
+ ///
+ /// Verify that AsIChatClientWithStoredOutputDisabled wraps the original ProjectResponsesClient,
+ /// which remains accessible via the service chain.
+ ///
+ [Fact]
+ public void AsIChatClientWithStoredOutputDisabled_InnerResponsesClientIsAccessible()
+ {
+ // Arrange
+ var responseClient = CreateTestClient();
+
+ // Act
+ var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled();
+
+ // Assert - the inner ProjectResponsesClient should be accessible via GetService
+ var innerClient = chatClient.GetService();
+ Assert.NotNull(innerClient);
+ Assert.Same(responseClient, innerClient);
+ }
+
+ ///
+ /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent false
+ /// wraps the original ProjectResponsesClient, which remains accessible via the service chain.
+ ///
+ [Fact]
+ public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningFalse_InnerResponsesClientIsAccessible()
+ {
+ // Arrange
+ var responseClient = CreateTestClient();
+
+ // Act
+ var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: false);
+
+ // Assert - the inner ProjectResponsesClient should be accessible via GetService
+ var innerClient = chatClient.GetService();
+ Assert.NotNull(innerClient);
+ Assert.Same(responseClient, innerClient);
+ }
+
+ ///
+ /// Verify that AsIChatClientWithStoredOutputDisabled with default parameter (includeReasoningEncryptedContent = true)
+ /// configures StoredOutputEnabled to false and includes ReasoningEncryptedContent in IncludedProperties.
+ ///
+ [Fact]
+ public void AsIChatClientWithStoredOutputDisabled_Default_ConfiguresStoredOutputDisabledWithReasoningEncryptedContent()
+ {
+ // Arrange
+ var responseClient = CreateTestClient();
+
+ // Act
+ var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled();
+
+ // Assert
+ var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient);
+ Assert.NotNull(createResponseOptions);
+ Assert.False(createResponseOptions.StoredOutputEnabled);
+ Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
+ }
+
+ ///
+ /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent explicitly set to true
+ /// configures StoredOutputEnabled to false and includes ReasoningEncryptedContent in IncludedProperties.
+ ///
+ [Fact]
+ public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningTrue_ConfiguresStoredOutputDisabledWithReasoningEncryptedContent()
+ {
+ // Arrange
+ var responseClient = CreateTestClient();
+
+ // Act
+ var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: true);
+
+ // Assert
+ var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient);
+ Assert.NotNull(createResponseOptions);
+ Assert.False(createResponseOptions.StoredOutputEnabled);
+ Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
+ }
+
+ ///
+ /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent set to false
+ /// configures StoredOutputEnabled to false and does not include ReasoningEncryptedContent in IncludedProperties.
+ ///
+ [Fact]
+ public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningFalse_ConfiguresStoredOutputDisabledWithoutReasoningEncryptedContent()
+ {
+ // Arrange
+ var responseClient = CreateTestClient();
+
+ // Act
+ var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: false);
+
+ // Assert
+ var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient);
+ Assert.NotNull(createResponseOptions);
+ Assert.False(createResponseOptions.StoredOutputEnabled);
+ Assert.DoesNotContain(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
+ }
+
+ ///
+ /// Verify that AsIChatClientWithStoredOutputDisabled works with an optional deployment name.
+ ///
+ [Fact]
+ public void AsIChatClientWithStoredOutputDisabled_WithDeploymentName_ConfiguresStoredOutputDisabled()
+ {
+ // Arrange
+ var responseClient = CreateTestClient();
+
+ // Act
+ var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(deploymentName: "my-deployment");
+
+ // Assert
+ var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient);
+ Assert.NotNull(createResponseOptions);
+ Assert.False(createResponseOptions.StoredOutputEnabled);
+ Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties);
+ }
+
+ ///
+ /// Extracts the produced by the ConfigureOptions pipeline
+ /// by using reflection to access the configure action and invoking it on a test .
+ ///
+ private static CreateResponseOptions? GetCreateResponseOptionsFromPipeline(IChatClient chatClient)
+ {
+ var configureField = chatClient.GetType().GetField("_configureOptions", BindingFlags.NonPublic | BindingFlags.Instance);
+ Assert.NotNull(configureField);
+
+ var configureAction = configureField.GetValue(chatClient) as Action;
+ Assert.NotNull(configureAction);
+
+ var options = new ChatOptions();
+ configureAction(options);
+
+ Assert.NotNull(options.RawRepresentationFactory);
+ return options.RawRepresentationFactory(chatClient) as CreateResponseOptions;
+ }
+}
From 25334b1a2c8065c490047d4fa6e220557c5df1f2 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Wed, 25 Mar 2026 20:27:49 +0000
Subject: [PATCH 2/2] Preserve existing RawRepresentationFactory when disabling
stored output
Address PR review feedback: wrap/chain the existing factory instead of
replacing it, so upstream configuration (e.g., deploymentName/model defaults
from AsIChatClient) is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../ProjectResponsesClientExtensions.cs | 21 ++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs
index 25869ca1eb..5a899d5076 100644
--- a/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/ProjectResponsesClientExtensions.cs
@@ -37,9 +37,24 @@ public static IChatClient AsIChatClientWithStoredOutputDisabled(this ProjectResp
return Throw.IfNull(responseClient)
.AsIChatClient(deploymentName)
.AsBuilder()
- .ConfigureOptions(x => x.RawRepresentationFactory = _ => includeReasoningEncryptedContent
- ? new CreateResponseOptions() { StoredOutputEnabled = false, IncludedProperties = { IncludedResponseProperty.ReasoningEncryptedContent } }
- : new CreateResponseOptions() { StoredOutputEnabled = false })
+ .ConfigureOptions(x =>
+ {
+ var previousFactory = x.RawRepresentationFactory;
+ x.RawRepresentationFactory = state =>
+ {
+ var responseOptions = previousFactory?.Invoke(state) as CreateResponseOptions ?? new CreateResponseOptions();
+
+ responseOptions.StoredOutputEnabled = false;
+
+ if (includeReasoningEncryptedContent &&
+ !responseOptions.IncludedProperties.Contains(IncludedResponseProperty.ReasoningEncryptedContent))
+ {
+ responseOptions.IncludedProperties.Add(IncludedResponseProperty.ReasoningEncryptedContent);
+ }
+
+ return responseOptions;
+ };
+ })
.Build();
}
}