Skip to content

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

Open
rogerbarreto wants to merge 5 commits intomicrosoft:mainfrom
rogerbarreto:feature/hosted-user-agent-meai-10.5.1
Open

.NET: Bump MEAI to 10.5.1 and add Foundry per-call x-client header support#5652
rogerbarreto wants to merge 5 commits intomicrosoft: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.
Copilot AI review requested due to automatic review settings May 5, 2026 13:08
@moonbox3 moonbox3 added the .NET label May 5, 2026
@rogerbarreto 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 support May 5, 2026
@github-actions github-actions Bot 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 support May 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
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.

Comment thread dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs Outdated
@rogerbarreto 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 support May 5, 2026
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

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 thread dotnet/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.
@rogerbarreto rogerbarreto enabled auto-merge May 6, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

4 participants