Skip to content

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

Merged
rogerbarreto merged 6 commits intomicrosoft: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
rogerbarreto merged 6 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
Comment thread dotnet/src/Microsoft.Agents.AI.Foundry/ClientHeadersScope.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj Outdated
* Add AIOpenAIRequestPolicies = MEAIExperiments alias to
  DiagnosticIds.Experiments (matches the existing AIResponseContinuations,
  AIMcpServers, AIFunctionApprovals pattern).
* Mark public ClientHeadersExtensions with [Experimental(AIOpenAIRequestPolicies)]
  instead of AgentsAIExperiments. Consumers now see the MEAI001 warning,
  surfacing the dependency on MEAI's experimental OpenAIRequestPolicies hook.
* Mark internal OpenAIRequestPoliciesReflection with the same alias to
  suppress warnings at the source rather than via project-wide NoWarn.
* Remove MEAI001 from Foundry csproj NoWarn (kept on Foundry.Hosting where
  pre-PR usages remain).
* Clarify ClientHeadersScope XML doc: AsyncLocal flows values forward but
  does NOT auto-restore on method return; explicit using/Dispose is what
  gives stack-style LIFO semantics.
@rogerbarreto rogerbarreto added this pull request to the merge queue May 6, 2026
Merged via the queue into microsoft:main with commit b12109b May 6, 2026
23 checks passed
@github-project-automation github-project-automation Bot moved this from In Review to Done in Agent Framework May 6, 2026
rogerbarreto added a commit to rogerbarreto/agent-framework-public that referenced this pull request May 6, 2026
…ectly to fix hosted-agent URL routing

Fixes the experimental FoundryAgent(Uri agentEndpoint, AuthenticationTokenProvider, ...)
constructor so it actually works against Foundry hosted agents.

The previous implementation routed through AzureAIProjectChatClient, which
internally called aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClientForAgent(...).
For an agent-endpoint URL of the canonical shape

  https://<host>/api/projects/<project>/agents/<agentName>/endpoint/protocols/openai

the chain produced

  POST https://<host>/api/projects/<project>/openai/v1/responses

(project-level path, no /agents/ segment). The Foundry service rejects this with
HTTP 400 "Hosted agents can only be called through the agent endpoint:
.../agents/<agentName>/endpoint/protocols/openai/responses".

The constructor also extracted the agent name via
agentEndpoint.Segments[^1].TrimEnd('/'), which returns "openai" (the last segment),
not the agent name.

What changed
- Public ctor signature: clientOptions parameter type changed from
  AIProjectClientOptions? to ProjectOpenAIClientOptions?. The constructor is
  fundamentally building a ProjectOpenAIClient; accepting AIProjectClientOptions
  was a leaky abstraction whose translation silently dropped any pipeline
  policies the caller added via AddPolicy(...). With the direct type, caller
  policies pass through to the per-agent traffic verbatim.
- Per-agent client construction: `new ProjectOpenAIClient(BearerTokenPolicy, ProjectOpenAIClientOptions)`
  with Endpoint and AgentName set, then `GetProjectResponsesClient().AsIChatClient()`.
  The SDK auto-appends ?api-version=v1 when AgentName is set.
- New private static ParseAgentEndpoint helper: single source of truth for both
  agent-name extraction and project-root derivation. Tolerates trailing slash,
  case variants on /agents/ and the suffix segment, strips query/fragment, and
  throws ArgumentException with paramName=nameof(agentEndpoint) for malformed input.
- Project-level client (used by CreateConversationSessionAsync) is built fresh
  from the derived project root with primitive properties copied
  (RetryPolicy/NetworkTimeout/Transport/UserAgentApplicationId) plus MEAI UA.
- New GetService<ProjectOpenAIClient>() entry alongside the existing
  GetService<AIProjectClient>() (the latter returns null in agent-endpoint mode
  since no AIProjectClient is constructed on that path).
- Endpoint and AgentName on caller-supplied ProjectOpenAIClientOptions are
  overridden by values derived from agentEndpoint.

Compatibility
- FoundryAgent is [Experimental(OPENAI001)]. No GA surface touched. The Foundry
  project does not maintain PublicAPI.*.txt baselines so there is no shipped
  baseline to update.
- The Microsoft.Agents.AI.Foundry csproj pins
  Azure.AI.Projects to VersionOverride 2.1.0-beta.1 (matching what the IT and
  hosting projects already use); the central pin in Directory.Packages.props
  stays at 2.0.0.
- WireClientHeaders from PR microsoft#5652 is invoked on the agent-endpoint path so
  per-call x-client-* headers behave identically across both ctors.

Tests
- 23 new unit tests in FoundryAgentTests.cs:
  - 12 for the agent-endpoint constructor (URL routing for non-streaming and
    streaming, conversations URL shape, MEAI UA stamping, caller-policy
    passthrough on the per-agent pipeline, Endpoint/AgentName override
    semantics, GetService matrix, ProjectOpenAIClient propagation,
    UserAgentApplicationId propagation, null-arg validation, ID/Name slug)
  - 9 for ParseAgentEndpoint (standard shape, trailing slash, casing,
    sovereign-cloud host without /api/projects/ literal prefix, special chars
    in agent name, query/fragment stripping, three negative cases)
  - 2 null-arg tests for the public ctor
- All 250 Microsoft.Agents.AI.Foundry.UnitTests pass (was 221 baseline plus
  29 from PR microsoft#5652 plus 23 new in this PR equals 273; pre-existing tests
  collapsed by the rebase merge keep the total at 250).
- All 225 Microsoft.Agents.AI.Foundry.Hosting.UnitTests pass; no behavioral
  change to the hosting layer.
- dotnet build clean across net8/9/10/netstandard2.0/net472 with
  TreatWarningsAsErrors=true.
- dotnet format --verify-no-changes clean for the touched src and test projects.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants