Skip to content

.NET: Bump MEAI to 10.5.1 and add Foundry per-call x-client header support - #5652

Merged
Roger Barreto (rogerbarreto) merged 6 commits into
microsoft:mainfrom
rogerbarreto:feature/hosted-user-agent-meai-10.5.1
May 6, 2026
Merged

.NET: Bump MEAI to 10.5.1 and add Foundry per-call x-client header support#5652
Roger Barreto (rogerbarreto) merged 6 commits into
microsoft:mainfrom
rogerbarreto:feature/hosted-user-agent-meai-10.5.1

Conversation

@rogerbarreto

Copy link
Copy Markdown
Member

Summary

Bumps Microsoft.Extensions.AI from 10.5.0 to 10.5.1 and replaces the brittle UserAgentResponsesClient subclass with a clean per-call x-client-* header pipeline built on the new MEAI 10.5.1 OpenAIRequestPolicies hook.

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-RunAsync basis, 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);
`

  • Header names must start with x-client- (case-insensitive); other names throw ArgumentException.
  • Bulk WithClientHeaders validates all entries before mutating any (all-or-nothing).
  • If the ChatOptions.AdditionalProperties carrier slot is already occupied by a non-dictionary value, throws InvalidOperationException.

Internals

  • ClientHeadersAgent decorator snapshots the dict at scope-push time so concurrent runs sharing a ChatOptions reference do not cross-contaminate.
  • ClientHeadersScope is an AsyncLocal<IReadOnlyDictionary<string,string>?> with LIFO using semantics.
  • ClientHeadersPolicy singleton stamps headers with Headers.Set so per-call values overwrite same-name headers from earlier policies and double registration is value-stable.
  • OpenAIRequestPoliciesReflection dedups against MEAI's private _entries field 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

  • Deleted UserAgentResponsesClient (~115 lines) 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.

Verification

  • Full solution build: 0 errors, 0 warnings.
  • CI-parity dotnet format --verify-no-changes (Docker mcr.microsoft.com/dotnet/sdk:10.0) clean on all 5 changed projects.
  • 5245 tests passing across Foundry, Foundry.Hosting, core AI, Abstractions, AzureAI.Persistent, OpenAI, Anthropic.

Notes

  • Microsoft.Extensions.Compliance.Abstractions stays at 10.5.0 because no 10.5.1 release exists on nuget.org.
  • The OpenAI bridge is reachable through Foundry today; if a non-Foundry consumer wants the same wire delivery, they reference Microsoft.Agents.AI.Foundry. Splitting the bridge into its own package can come later if there is demand.

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.
CopilotAI review requested due to automatic review settings May 5, 2026 13:08
@moonbox3Evan Mattson (moonbox3) added the .NET Usage: [Issues, PRs], Target: .Net label May 5, 2026
@rogerbarretoRoger Barreto (rogerbarreto) changed the title Bump MEAI to 10.5.1 and add per-call x-client header support.Net: Bump MEAI to 10.5.1 and add per-call x-client header supportMay 5, 2026
@github-actionsgithub-actionsBot changed the title .Net: Bump MEAI to 10.5.1 and add per-call x-client header support.NET: Bump MEAI to 10.5.1 and add per-call x-client header supportMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and ClientHeadersPolicy to carry per-call x-client-* headers from ChatOptions to outbound OpenAI requests.
  • Removed the UserAgentResponsesClient wrapper and switched hosted User-Agent injection to direct OpenAIRequestPolicies registration.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
dotnet/tests/Microsoft.Agents.AI.UnitTests/OpenTelemetryAgentTests.csUpdated telemetry expectations for MEAI 10.5.1 tool serialization.
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csprojExcludes new client-header tests on older target frameworks.
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/ClientHeadersExtensionsTests.csAdds extensive unit coverage for client-header validation, scoping, and wire stamping.
dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/UserAgentResponsesClientTests.csRemoves tests for the deleted wrapper-based user-agent implementation.
dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csprojSuppresses MEAI experimental warnings for new APIs.
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.csPre-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.csAdds AsyncLocal scope carrier for per-call header propagation.
dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersPolicy.csAdds outbound header-stamping policy plus reflection-based OpenAI policy dedup helpers.
dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersExtensions.csAdds public API for attaching per-call client headers and opting agents into the pipeline.
dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersAgent.csAdds delegating agent that snapshots and scopes client headers for each run.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/UserAgentResponsesClient.csDeletes the old ResponsesClient subclass wrapper.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.csSwitches hosted user-agent injection to policy registration via OpenAIRequestPolicies.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.csUpdates docs to reflect the new registration path.
dotnet/Directory.Packages.propsBumps MEAI package versions to 10.5.1.

Comment threaddotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs Outdated
@rogerbarretoRoger Barreto (rogerbarreto) changed the title .NET: Bump MEAI to 10.5.1 and add per-call x-client header support.NET: Bump MEAI to 10.5.1 and add Foundry per-call x-client header supportMay 5, 2026

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hosted User-Agent hook 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 FoundryAgent constructor helpers changed here, but the existing public AIProjectClient.AsAIAgent(AgentReference|ProjectsAgentRecord|ProjectsAgentVersion) overloads still create a plain ChatClientAgent and pass it through the unchanged internal FoundryAgent(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.
Comment threaddotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersAgent.cs Outdated
ct is already passed to InnerAgent.RunStreamingAsync, so
.WithCancellation(ct) on the resulting IAsyncEnumerable is a no-op.
Caught by Sergey on PR review.
Comment threaddotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersScope.cs Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NETUsage: [Issues, PRs], Target: .Net

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

.NET: [Feature]: Update Microsoft.Extensions.AI to version 10.5.1

5 participants

@rogerbarreto@SergeyMenshykh@westey-m@moonbox3