.NET: Bump MEAI to 10.5.1 and add Foundry per-call x-client header support#5652
.NET: Bump MEAI to 10.5.1 and add Foundry per-call x-client header support#5652rogerbarreto wants to merge 5 commits intomicrosoft:mainfrom
Conversation
Replaces the brittle UserAgentResponsesClient subclass with a clean per-call x-client-* header pipeline built on the new Microsoft.Extensions.AI 10.5.1 OpenAIRequestPolicies hook. Public surface (Microsoft.Agents.AI.Foundry, [Experimental(MAAI001)]): * chatOptions.WithClientHeader(name, value) and .WithClientHeaders(IEnumerable) validate the x-client- prefix (case-insensitive), apply all-or-nothing on bulk, and throw InvalidOperationException on foreign-typed slot collision * myAgent.AsBuilder().UseClientHeaders().Build() opts a customer-built agent into the pipeline; idempotent via agent.GetService<ClientHeadersAgent>() * Foundry-built agents (FoundryAgent.Create*) pre-wire automatically Internals: * ClientHeadersAgent decorator snapshots the dict at scope-push time so concurrent runs sharing a ChatOptions reference do not leak headers * ClientHeadersScope is an AsyncLocal<IReadOnlyDictionary<string,string>?> with LIFO push/dispose semantics * ClientHeadersPolicy singleton stamps headers via Headers.Set so per-call values overwrite any same-name header from earlier policies and so duplicate registration is value-stable * OpenAIRequestPoliciesReflection dedups against MEAI's private _entries field and falls back to AddPolicy on any reflection failure; a CI test asserts the field shape on every MEAI bump Hosting cleanup: * Deleted UserAgentResponsesClient and its dummy throwing pipeline * HostedAgentUserAgentPolicy is now registered via OpenAIRequestPolicies in FoundryHostingExtensions.TryApplyUserAgent Tests: * 19 new unit tests in ClientHeadersExtensionsTests.cs covering validation, AsyncLocal isolation, snapshot semantics, end-to-end wire stamping, and shared-chat-client dedup * Updated OpenTelemetryAgentTests for MEAI 10.5.1 changes to web_search serialization and the reduced tool definition payload when sensitive data capture is disabled Microsoft.Extensions.Compliance.Abstractions stays at 10.5.0 because no 10.5.1 release exists on nuget.org.
There was a problem hiding this comment.
Pull request overview
Upgrades the .NET Foundry integration to Microsoft.Extensions.AI 10.5.1 and introduces a new per-run x-client-* header forwarding mechanism using MEAI's OpenAIRequestPolicies hook, while also replacing the old hosted User-Agent wrapper with pipeline policies.
Changes:
- Bumped MEAI packages to 10.5.1 and updated telemetry tests for the new serialized tool payload shape.
- Added
ClientHeadersExtensions,ClientHeadersAgent,ClientHeadersScope, andClientHeadersPolicyto carry per-callx-client-*headers fromChatOptionsto outbound OpenAI requests. - Removed the
UserAgentResponsesClientwrapper and switched hostedUser-Agentinjection to directOpenAIRequestPoliciesregistration.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.cs | Updated telemetry expectations for MEAI 10.5.1 tool serialization. |
| dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj | Excludes new client-header tests on older target frameworks. |
| dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/ClientHeadersExtensionsTests.cs | Adds extensive unit coverage for client-header validation, scoping, and wire stamping. |
| dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/UserAgentResponsesClientTests.cs | Removes tests for the deleted wrapper-based user-agent implementation. |
| dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj | Suppresses MEAI experimental warnings for new APIs. |
| dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs | Pre-wires client-header policy/decorator in constructor-based Foundry agents and updates session access to walk the delegating chain. |
| dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersScope.cs | Adds AsyncLocal scope carrier for per-call header propagation. |
| dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersPolicy.cs | Adds outbound header-stamping policy plus reflection-based OpenAI policy dedup helpers. |
| dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersExtensions.cs | Adds public API for attaching per-call client headers and opting agents into the pipeline. |
| dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersAgent.cs | Adds delegating agent that snapshots and scopes client headers for each run. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/UserAgentResponsesClient.cs | Deletes the old ResponsesClient subclass wrapper. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs | Switches hosted user-agent injection to policy registration via OpenAIRequestPolicies. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs | Updates docs to reflect the new registration path. |
| dotnet/Directory.Packages.props | Bumps MEAI package versions to 10.5.1. |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 82%
✓ Correctness
The PR replaces the reflection-heavy UserAgentResponsesClient subclass approach with the cleaner MEAI 10.5.1 OpenAIRequestPolicies hook. The service resolution chain (DelegatingAIAgent.GetService walks InerAgent) correctly finds ChatClientAgent through the new ClientHeadersAgent wrapper. The OpenTelemetry test updates match MEAI 10.5.1 behavioral changes (web_search gains a name field, function tools omit description/parameters when sensitive data is disabled). One minor correctness concern in the streaming path: when no client headers are present, the
default(Scope)struct's Dispose will unconditionally set the AsyncLocal to null, which could clear an outer scope in hypothetical nested-agent scenarios.
✓ Security Reliability
This PR replaces fragile reflection-based ResponsesClient wrapping with MEAI 10.5.1's public OpenAIRequestPolicies hook and adds a new per-call x-client-* header pipeline. The design is sound: header validation restricts to the x-client- prefix, AsyncLocal isolates concurrent runs, reflection helpers degrade gracefully, and wire-level idempotency guards prevent double-stamping. One minor asymetry exists in the streaming path where a default(Scope) Dispose could theoretically clobber an outer AsyncLocal value, but the practical risk is negligible since ClientHeadersAgent is internal and UseClientHeaders deduplicates.
✓ Test Coverage
The PR introduces significant new functionality (ClientHeadersAgent, ClientHeadersPolicy, ClientHeadersScope, ClientHeadersExtensions) and replaces the old UserAgentResponsesClient approach with the MEAI 10.5.1 OpenAIRequestPolicies hook. Test coverage is strong: the new 706-line ClientHeadersExtensionsTests.cs file provides 19 tests covering input validation, decorator behavior, AsyncLocal scoping, pipeline policy stamping, reflection dedup, end-to-end wire validation, and idempotency. The deleted UserAgentResponsesClientTests.cs (452 lines) tested code that no longer exists. One moderate gap: the HostedAgentUserAgentPolicy retry-idempotency scenario previously tested in Polyfill_RetryWithinCall_DoesNotDuplicateSupplementInUserAgentAsync has no replacement test, though the policy code is unchanged and its Contains-check still provides the dedup guarantee.
✗ Design Approach
I found two design-level problems. First, the new client-header forwarding path is only wired into some Foundry construction paths, so callers get different behavior depending on which public
AIProjectClient.AsAIAgent(...)overload they use. Second, the hostedUser-Agenthook now registers its policy on every agent resolution, which is safe on the wire but causes duplicate pipeline entries to accumulate on long-lived agents.
Flagged Issues
- The new client-header bridge is only installed in the
FoundryAgentconstructor helpers changed here, but the existing publicAIProjectClient.AsAIAgent(AgentReference|ProjectsAgentRecord|ProjectsAgentVersion)overloads still create a plainChatClientAgentand pass it through the unchanged internalFoundryAgent(AIProjectClient, ChatClientAgent)constructor (AzureAIProjectChatClientExtensions.cs:54-66, 91-99, 124-132, 220-245;FoundryAgent.cs:104-108). Header forwarding should not depend on which public factory overload the caller picks — centralize this wiring in shared Foundry agent construction so all factory paths behave the same.
Automated review by rogerbarreto's agents
* FoundryAgent: extract WireClientHeaders helper and call it from the internal (AIProjectClient, ChatClientAgent) constructor used by AzureAIProjectChatClientExtensions.AsAIAgent so those Foundry-built agents also pre-wire the x-client header pipeline. * Foundry.Hosting TryApplyUserAgent: dedup HostedAgentUserAgentPolicy registration per OpenAIRequestPolicies instance via ConditionalWeakTable so per-request resolution does not grow the policy list unboundedly on singleton agents.
Backs the PR review fixes from a4c8f91 with regression tests: * ClientHeadersExtensionsTests: AsAIAgent_FoundryAgent_HasPreWiredClientHeadersAgent asserts the FoundryAgent built via AzureAIProjectChatClientExtensions.AsAIAgent contains a ClientHeadersAgent in its delegating chain (catches future regressions of the bypass). * ClientHeadersExtensionsTests: FoundryAgent_PublicConstructor_HasPreWiredClientHeadersAgent covers the public constructor path the same way. * ClientHeadersExtensionsTests: UseClientHeaders_RepeatedRegistrations_OnSameChatClient_OnlyRegistersOnce invokes UseClientHeaders 25 times on a shared chat client and asserts via reflection that OpenAIRequestPolicies._entries length is exactly 1. * HostedTryApplyUserAgentDedupTests: two tests asserting FoundryHostingExtensions.TryApplyUserAgent stays at one entry per OpenAIRequestPolicies instance after 50 calls on the same agent and across distinct agents on different chat clients.
Removes the dedicated HostedTryApplyUserAgentDedupTests.cs test class. Tests are co-located with the SUT they exercise: * FoundryAgentTests.cs gains the Constructor_PreWiresClientHeadersAgent and Constructor_FromAsAIAgentExtension_PreWiresClientHeadersAgent cases, since FoundryAgent is the SUT for the pre-wire behavior. * HostedOutboundUserAgentTests.cs gains the two TryApplyUserAgent dedup cases, since FoundryHostingExtensions.TryApplyUserAgent is the SUT it already covers. * ClientHeadersExtensionsTests.cs keeps only the UseClientHeaders_RepeatedRegistrations_OnSameChatClient_OnlyRegistersOnce case, which exercises the public ClientHeadersExtensions surface.
ct is already passed to InnerAgent.RunStreamingAsync, so .WithCancellation(ct) on the resulting IAsyncEnumerable is a no-op. Caught by Sergey on PR review.
Summary
Bumps Microsoft.Extensions.AI from 10.5.0 to 10.5.1 and replaces the brittle
UserAgentResponsesClientsubclass with a clean per-callx-client-*header pipeline built on the new MEAI 10.5.1OpenAIRequestPolicieshook.The motivating scenario is the multi-tenant SaaS overlay from the Foundry Hosted Agents design: a SaaS backend needs to attest the end-user identity (
x-client-end-user-id) and chat surface (x-client-end-chat-id) on a per-RunAsyncbasis, without rebuilding the agent or the chat client.Public surface
All in
Microsoft.Agents.AI.Foundry, gated by[Experimental(MAAI001)].` csharp
// Per-run carrier on ChatOptions
chatOptions.WithClientHeader("x-client-end-user-id", "alice");
chatOptions.WithClientHeaders(new[] {
KeyValuePair.Create("x-client-end-user-id", "alice"),
KeyValuePair.Create("x-client-end-chat-id", "chat-42"),
});
// Opt-in for customer-built agents
var agent = new ChatClientAgent(chatClient).AsBuilder().UseClientHeaders().Build();
// Foundry-built agents pre-wire automatically
var foundryAgent = new FoundryAgent(endpoint, credential, model, instructions);
`
x-client-(case-insensitive); other names throwArgumentException.WithClientHeadersvalidates all entries before mutating any (all-or-nothing).ChatOptions.AdditionalPropertiescarrier slot is already occupied by a non-dictionary value, throwsInvalidOperationException.Internals
ClientHeadersAgentdecorator snapshots the dict at scope-push time so concurrent runs sharing aChatOptionsreference do not cross-contaminate.ClientHeadersScopeis anAsyncLocal<IReadOnlyDictionary<string,string>?>with LIFOusingsemantics.ClientHeadersPolicysingleton stamps headers withHeaders.Setso per-call values overwrite same-name headers from earlier policies and double registration is value-stable.OpenAIRequestPoliciesReflectiondedups against MEAI's private_entriesfield with graceful fallback. A CI-guardrail test asserts the field shape so future MEAI bumps fail loudly here rather than silently disabling dedup.Hosting cleanup
UserAgentResponsesClient(~115 lines) and its dummy throwing pipeline.HostedAgentUserAgentPolicyis now registered viaOpenAIRequestPoliciesinFoundryHostingExtensions.TryApplyUserAgent.Tests
ClientHeadersExtensionsTests.cscovering validation, AsyncLocal isolation, snapshot semantics, end-to-end wire stamping, and shared-chat-client dedup.OpenTelemetryAgentTestsfor MEAI 10.5.1 changes toweb_searchserialization and the reduced tool definition payload when sensitive data capture is disabled.Verification
dotnet format --verify-no-changes(Dockermcr.microsoft.com/dotnet/sdk:10.0) clean on all 5 changed projects.Notes
Microsoft.Extensions.Compliance.Abstractionsstays at 10.5.0 because no 10.5.1 release exists on nuget.org.