From 3b6a4574ebd4a9dba00ad2992db5e19dd78a3d01 Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Tue, 12 May 2026 10:16:47 -0700 Subject: [PATCH] .NET: Fix OpenAIResponsesAgentClient to include agentName in endpoint path (#5748) * Fix OpenAIResponsesAgentClient endpoint to include agentName in path (#5324) The sample OpenAIResponsesAgentClient used '/v1/' as the endpoint, which routes to the multi-agent endpoint requiring agent.name in the request body. However, AsIChatClient(agentName) maps agentName to the model field, not agent.name, causing HTTP 400 errors on OpenAI-compatible endpoints. Changed the endpoint to '/{agentName}/v1/' to match the pattern used by OpenAIChatCompletionsAgentClient, routing to the single-agent endpoint where no agent.name body field is needed. Added regression test verifying that the model field alone is insufficient for agent resolution on the multi-agent endpoint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #5324 - URL-escape agentName in OpenAIResponsesAgentClient endpoint path to handle reserved characters safely - Add per-agent MapOpenAIResponses() calls in AgentHost so the sample host serves the /{agentName}/v1/responses routes the client now targets - Replace brittle Assert.Contains("agent.name") assertions with stable machine-readable error code assertion ("missing_required_parameter") Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address additional review feedback for #5324 - Apply Uri.EscapeDataString to OpenAIChatCompletionsAgentClient endpoint for consistency with OpenAIResponsesAgentClient - Map OpenAI Responses and ChatCompletions endpoints for all builder-based agents (chemist, mathematician, literator, science workflows) so every discoverable agent is reachable via the single-agent endpoint path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AgentWebChat.AgentHost/Program.cs | 12 ++++++ .../OpenAIChatCompletionsAgentClient.cs | 2 +- .../OpenAIResponsesAgentClient.cs | 2 +- ...esponsesAgentResolutionIntegrationTests.cs | 38 ++++++++++++++++++- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs index d6957a9b8e..61cfdcdb68 100644 --- a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -163,10 +163,22 @@ app.MapA2AHttpJson(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves"); app.MapDevUI(); app.MapOpenAIResponses(); +app.MapOpenAIResponses(pirateAgentBuilder); +app.MapOpenAIResponses(knightsKnavesAgentBuilder); +app.MapOpenAIResponses(chemistryAgent); +app.MapOpenAIResponses(mathsAgent); +app.MapOpenAIResponses(literatureAgent); +app.MapOpenAIResponses(scienceSequentialWorkflow); +app.MapOpenAIResponses(scienceConcurrentWorkflow); app.MapOpenAIConversations(); app.MapOpenAIChatCompletions(pirateAgentBuilder); app.MapOpenAIChatCompletions(knightsKnavesAgentBuilder); +app.MapOpenAIChatCompletions(chemistryAgent); +app.MapOpenAIChatCompletions(mathsAgent); +app.MapOpenAIChatCompletions(literatureAgent); +app.MapOpenAIChatCompletions(scienceSequentialWorkflow); +app.MapOpenAIChatCompletions(scienceConcurrentWorkflow); // Map the agents HTTP endpoints app.MapAgentDiscovery("/agents"); diff --git a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIChatCompletionsAgentClient.cs b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIChatCompletionsAgentClient.cs index 8939ca785a..a90aac515e 100644 --- a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIChatCompletionsAgentClient.cs +++ b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIChatCompletionsAgentClient.cs @@ -24,7 +24,7 @@ internal sealed class OpenAIChatCompletionsAgentClient(HttpClient httpClient) : { OpenAIClientOptions options = new() { - Endpoint = new Uri(httpClient.BaseAddress!, $"/{agentName}/v1/"), + Endpoint = new Uri(httpClient.BaseAddress!, $"/{Uri.EscapeDataString(agentName)}/v1/"), Transport = new HttpClientPipelineTransport(httpClient) }; diff --git a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs index 839c8e75a1..db182db565 100644 --- a/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs +++ b/dotnet/samples/05-end-to-end/AgentWebChat/AgentWebChat.Web/OpenAIResponsesAgentClient.cs @@ -23,7 +23,7 @@ internal sealed class OpenAIResponsesAgentClient(HttpClient httpClient) : AgentC { OpenAIClientOptions options = new() { - Endpoint = new Uri(httpClient.BaseAddress!, "/v1/"), + Endpoint = new Uri(httpClient.BaseAddress!, $"/{Uri.EscapeDataString(agentName)}/v1/"), Transport = new HttpClientPipelineTransport(httpClient) }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs index 9ea9541ccb..a9d806ed05 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesAgentResolutionIntegrationTests.cs @@ -267,7 +267,43 @@ public sealed class OpenAIResponsesAgentResolutionIntegrationTests : IAsyncDispo Assert.Equal(System.Net.HttpStatusCode.BadRequest, httpResponse.StatusCode); string responseJson = await httpResponse.Content.ReadAsStringAsync(); - Assert.Contains("agent.name", responseJson, StringComparison.OrdinalIgnoreCase); + using JsonDocument errorDoc1 = JsonDocument.Parse(responseJson); + string? errorCode = errorDoc1.RootElement.GetProperty("error").GetProperty("code").GetString(); + Assert.Equal("missing_required_parameter", errorCode); + } + + /// + /// Verifies that the model field alone is not used for agent resolution. + /// The multi-agent endpoint requires agent.name or metadata.entity_id; setting only model returns 400. + /// + [Fact] + public async Task CreateResponse_WithModelOnly_ReturnsBadRequestAsync() + { + // Arrange + const string AgentName = "test-agent"; + + this._httpClient = await this.CreateTestServerWithAgentResolutionAsync( + (AgentName, "Instructions", "Response")); + + // Act - Send request with model=agentName but no agent.name or metadata.entity_id + using StringContent requestContent = new(JsonSerializer.Serialize(new + { + model = AgentName, + input = new[] + { + new { type = "message", role = "user", content = "Test message" } + } + }), Encoding.UTF8, "application/json"); + + using HttpResponseMessage httpResponse = await this._httpClient!.PostAsync(new Uri("/v1/responses", UriKind.Relative), requestContent); + + // Assert - model is not used for agent resolution + Assert.Equal(System.Net.HttpStatusCode.BadRequest, httpResponse.StatusCode); + + string responseJson = await httpResponse.Content.ReadAsStringAsync(); + using JsonDocument errorDoc2 = JsonDocument.Parse(responseJson); + string? errorCode = errorDoc2.RootElement.GetProperty("error").GetProperty("code").GetString(); + Assert.Equal("missing_required_parameter", errorCode); } ///