Skip to content

.NET: Foundry agent-endpoint constructor uses ProjectOpenAIClient directly to fix hosted-agent URL routing#5677

Draft
rogerbarreto wants to merge 2 commits intomicrosoft:mainfrom
rogerbarreto:features/foundryagent-hosted-rewire
Draft

.NET: Foundry agent-endpoint constructor uses ProjectOpenAIClient directly to fix hosted-agent URL routing#5677
rogerbarreto wants to merge 2 commits intomicrosoft:mainfrom
rogerbarreto:features/foundryagent-hosted-rewire

Conversation

@rogerbarreto
Copy link
Copy Markdown
Member

@rogerbarreto rogerbarreto commented May 6, 2026

Summary

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

The bug

For an agent-endpoint URL of the canonical shape

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

the previous implementation routed through AzureAIProjectChatClient, which internally called aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClientForAgent(...). That chain produced

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

(project-level path, no /agents/ segment). The Foundry service rejected 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.

The fix

Build a per-agent ProjectOpenAIClient directly via the (AuthenticationPolicy, ProjectOpenAIClientOptions) constructor with Endpoint and AgentName set, then GetProjectResponsesClient().AsIChatClient(). The SDK auto-appends ?api-version=v1 when AgentName is set.

Verified live against it-happy-path on the Tao project: returns ECHO. The same wire-capture now shows the URL is …/agents/<name>/endpoint/protocols/openai/responses?api-version=v1.

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.
  • Endpoint and AgentName on caller-supplied ProjectOpenAIClientOptions are overridden by values derived from agentEndpoint (documented in the XML doc).
  • New private static ParseAgentEndpoint helper: single source of truth for both agent-name extraction and project-root derivation. Tolerates trailing slash, casing 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. Caller AddPolicy does not propagate to this pipeline (ClientPipelineOptions does not publicly enumerate its policies); documented as a limitation.
  • New GetService<ProjectOpenAIClient>() entry alongside the existing GetService<AIProjectClient>(). The latter now returns null in agent-endpoint mode since no AIProjectClient is constructed on that path.
  • WireClientHeaders from .NET: Bump MEAI to 10.5.1 and add Foundry per-call x-client header support #5652 is invoked on the agent-endpoint path so per-call x-client-* headers behave identically across both ctors.

Compatibility

  • FoundryAgent carries [Experimental(OPENAI001)]. No GA surface touched. The Microsoft.Agents.AI.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.

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.

Verification

  • Γ£à All 250 Microsoft.Agents.AI.Foundry.UnitTests pass.
  • Γ£à All 225 Microsoft.Agents.AI.Foundry.Hosting.UnitTests pass; no behavioral change to the hosting layer.
  • Γ£à dotnet build clean across net8/net9/net10/netstandard2.0/net472 with TreatWarningsAsErrors=true.
  • Γ£à dotnet format --verify-no-changes clean for the touched src and test projects.

Copilot AI review requested due to automatic review settings May 6, 2026 15:59
@moonbox3 moonbox3 added the .NET label May 6, 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: 91% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach


Automated review by rogerbarreto's agents

…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.
…rosoft.Agents.AI.Foundry to preview

Required to fix the NU1109 downgrade chain that broke CI on the agent-endpoint
constructor rewire (microsoft#5677). Microsoft.Agents.AI.Foundry now depends on
ProjectOpenAIClientOptions.AgentName and the (AuthenticationPolicy, options)
constructor that only exist in Azure.AI.Projects 2.1.0-beta.1.

Changes:
* Directory.Packages.props: Azure.AI.Projects 2.0.0 -> 2.1.0-beta.1.
* Microsoft.Agents.AI.Foundry.csproj: drop IsReleased=true so the package ships
  as preview (matches the beta SDK we now depend on). Add a comment noting the
  flip is temporary and should revert once Azure.AI.Projects ships a stable
  2.1.0.
* Drop redundant VersionOverride="2.1.0-beta.1" from the 10 csprojs that had it
  as a workaround; the central pin now suffices.

Verified:
* dotnet build agent-framework-dotnet.slnx --warnaserror clean across all TFMs.
* Microsoft.Agents.AI.Foundry.UnitTests 250/250 pass.
* Microsoft.Agents.AI.Foundry.Hosting.UnitTests 211/211 pass.
* dotnet format --verify-no-changes clean for the touched src and test projects.
@rogerbarreto rogerbarreto force-pushed the features/foundryagent-hosted-rewire branch from 8428195 to 7520c08 Compare May 6, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants