* .NET: Foundry agent-endpoint constructor uses ProjectOpenAIClient directly 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 #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 #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.
* .NET: Bump central Azure.AI.Projects pin to 2.1.0-beta.1 and flip Microsoft.Agents.AI.Foundry to preview
Required to fix the NU1109 downgrade chain that broke CI on the agent-endpoint
constructor rewire (#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.
Hosted-Observability
A hosted Agent Framework agent that demonstrates how the Foundry hosting pipeline emits OpenTelemetry traces, metrics and logs with no extra wiring.
The agent has two small tools, GetCurrentLocation and GetWeather, so an end-to-end run produces a span tree covering agent invocation, the underlying chat call, and tool execution.
How it works
Instrumentation is on by default
Unlike the Python SDK, the .NET hosting library is instrumented by default. AddFoundryResponses(agent) automatically wraps the agent with OpenTelemetryAgent (see Microsoft.Agents.AI.Foundry.Hosting.ServiceCollectionExtensions.ApplyOpenTelemetry) and the OTLP exporter pipeline is registered by Azure.AI.AgentServer.Core's AddAgentHostTelemetry(). There is no ENABLE_INSTRUMENTATION flag to set.
Sensitive content
Prompt, completion and tool argument content are omitted from spans by default. Set the OpenTelemetry standard environment variable to capture them:
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
This is the .NET equivalent of the Python sample's ENABLE_SENSITIVE_DATA. It is read by OpenTelemetryAgent.EnableSensitiveData.
Where the telemetry goes
Foundry injects APPLICATIONINSIGHTS_CONNECTION_STRING when the agent runs in the hosted environment, so traces, metrics and logs flow to Application Insights with no code change. To send telemetry from a local run, set the connection string yourself in .env.
Prerequisites
- .NET 10 SDK
- An Azure AI Foundry project with a deployed model (e.g.,
gpt-4o) - Azure CLI logged in (
az login)
Configuration
cp .env.example .env
Edit .env and set your Azure AI Foundry project endpoint:
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
Note:
.envis gitignored. The.env.exampletemplate is checked in as a reference.
Running directly (contributors)
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability
AGENT_NAME=hosted-observability dotnet run
The agent starts on http://localhost:8088.
Test it
azd ai agent invoke --local "What is the current weather where I am?"
Or with curl:
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "What is the current weather where I am?", "model": "hosted-observability"}'
Expected span tree
A single request produces approximately the following spans:
| Span | Source |
|---|---|
invoke_agent |
Outer span emitted by the Azure AI AgentServer hosting SDK |
agent_invoke <name> |
Emitted by OpenTelemetryAgent for each agent invocation |
chat <model> |
Emitted by the underlying IChatClient for each model call |
execute_tool <tool> |
Emitted for each invocation of GetCurrentLocation / GetWeather |
See the OpenTelemetry GenAI semantic conventions for the attributes captured on each span.
Running with Docker
This project uses ProjectReference to the local Agent Framework source, so use Dockerfile.contributor with a pre-published output:
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
docker build -f Dockerfile.contributor -t hosted-observability .
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-observability \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-observability
Deploying to Foundry and viewing traces
Once deployed, telemetry flows to the Application Insights instance attached to your Foundry project. In the Foundry UI, the Traces tab next to Playground lists conversations and lets you drill into the span tree for any request.
NuGet package users
If consuming the Agent Framework as a NuGet package, use the standard Dockerfile instead of Dockerfile.contributor. See the commented section in HostedObservability.csproj for the PackageReference alternative.