diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs index 2735325864..a0f53b342e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System; using System.ClientModel.Primitives; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Azure.AI.AgentServer.Responses; using Azure.Core; using Azure.Identity; @@ -230,12 +231,21 @@ public static class FoundryHostingExtensions var chatClient = agent.GetService(); if (chatClient?.GetService() is { } policies) { - // The HostedAgentUserAgentPolicy is idempotent on the wire (it skips when the - // supplement is already present in the User-Agent header), so we can register it - // unconditionally without dedup. MEAI's lock-free CAS-append on _entries is safe. - policies.AddPolicy(HostedAgentUserAgentPolicy.Instance, PipelinePosition.PerCall); + // Hosted agents are typically singletons resolved per request, so AddPolicy must be + // called at most once per OpenAIRequestPolicies instance to avoid unbounded growth of + // the policy list (each entry adds per-request CPU work even though the User-Agent + // value stays stable). Track which instances we have already wired with a + // ConditionalWeakTable keyed on the OpenAIRequestPolicies reference; the table holds + // weak references so it does not extend the lifetime of the chat client. + if (s_userAgentRegistrations.TryAdd(policies, s_boxedTrue)) + { + policies.AddPolicy(HostedAgentUserAgentPolicy.Instance, PipelinePosition.PerCall); + } } return agent; } + + private static readonly object s_boxedTrue = new(); + private static readonly ConditionalWeakTable s_userAgentRegistrations = new(); } diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs index c2052cf75d..db66b30a88 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs @@ -102,7 +102,7 @@ public sealed class FoundryAgent : DelegatingAIAgent /// Internal constructor used by AsAIAgent extension methods that already have an and a configured . /// internal FoundryAgent(AIProjectClient aiProjectClient, ChatClientAgent innerAgent) - : base(Throw.IfNull(innerAgent)) + : base(WireClientHeaders(Throw.IfNull(innerAgent))) { this._aiProjectClient = Throw.IfNull(aiProjectClient); } @@ -166,7 +166,7 @@ public sealed class FoundryAgent : DelegatingAIAgent #region Private helpers - private static ClientHeadersAgent CreateInnerAgent( + private static AIAgent CreateInnerAgent( AIProjectClient aiProjectClient, string model, string instructions, string? name, string? description, @@ -196,7 +196,7 @@ public sealed class FoundryAgent : DelegatingAIAgent return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); } - private static ClientHeadersAgent CreateResponsesChatClientAgent( + private static AIAgent CreateResponsesChatClientAgent( AIProjectClient aiProjectClient, ChatClientAgentOptions agentOptions, Func? clientFactory, @@ -215,9 +215,25 @@ public sealed class FoundryAgent : DelegatingAIAgent chatClient = clientFactory(chatClient); } - // Register the ClientHeadersPolicy on the chat client's OpenAIRequestPolicies, if available. - // Silent no-op when the chat client is not OpenAI-backed. - if (chatClient.GetService() is { } policies) + return WireClientHeaders(new ChatClientAgent(chatClient, agentOptions, loggerFactory, services)); + } + + /// + /// Registers on the agent's underlying chat client (if it + /// exposes ) and wraps the agent in a + /// so per-call x-client-* headers stamped via + /// reach + /// the wire. Idempotent: if the chain already contains a , + /// the original instance is returned unchanged. + /// + private static AIAgent WireClientHeaders(ChatClientAgent innerAgent) + { + if (innerAgent.GetService() is not null) + { + return innerAgent; + } + + if (innerAgent.ChatClient.GetService() is { } policies) { OpenAIRequestPoliciesReflection.AddPolicyIfMissing( policies, @@ -225,11 +241,10 @@ public sealed class FoundryAgent : DelegatingAIAgent System.ClientModel.Primitives.PipelinePosition.PerCall); } - var inner = new ChatClientAgent(chatClient, agentOptions, loggerFactory, services); - return new ClientHeadersAgent(inner); + return new ClientHeadersAgent(innerAgent); } - private static ClientHeadersAgent CreateInnerAgentFromEndpoint( + private static AIAgent CreateInnerAgentFromEndpoint( AIProjectClient aiProjectClient, Uri agentEndpoint, IList? tools, @@ -254,16 +269,7 @@ public sealed class FoundryAgent : DelegatingAIAgent chatClient = clientFactory(chatClient); } - if (chatClient.GetService() is { } policies) - { - OpenAIRequestPoliciesReflection.AddPolicyIfMissing( - policies, - ClientHeadersPolicy.Instance, - System.ClientModel.Primitives.PipelinePosition.PerCall); - } - - var inner = new ChatClientAgent(chatClient, agentOptions, services: services); - return new ClientHeadersAgent(inner); + return WireClientHeaders(new ChatClientAgent(chatClient, agentOptions, services: services)); } private static AIProjectClient CreateProjectClient(Uri endpoint, AuthenticationTokenProvider credential, AIProjectClientOptions? clientOptions = null)