From ef545bedfb758129054f17050bacddd726daee16 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:43:43 +0100 Subject: [PATCH 1/3] .NET: Remove Azure.AI.OpenAI dependency --- .../prompts/SemanticKernelToAgentFramework.md | 70 ++++++++++++------- dotnet/Directory.Packages.props | 3 +- dotnet/README.md | 14 ++-- .../A2AAgent_PollingForTaskCompletion.csproj | 1 - .../A2AAgent_StreamReconnection.csproj | 3 +- .../Step01_GettingStarted/Server/Program.cs | 14 ++-- .../Server/Server.csproj | 3 +- .../Step02_BackendTools/Server/Program.cs | 14 ++-- .../Step02_BackendTools/Server/Server.csproj | 3 +- .../Step03_FrontendTools/Server/Program.cs | 14 ++-- .../Step03_FrontendTools/Server/Server.csproj | 3 +- .../AGUI/Step04_HumanInLoop/Server/Program.cs | 14 ++-- .../Step04_HumanInLoop/Server/Server.csproj | 3 +- .../Step05_StateManagement/Server/Program.cs | 14 ++-- .../Server/Server.csproj | 3 +- ...gent_With_AzureOpenAIChatCompletion.csproj | 3 +- .../Program.cs | 14 ++-- .../README.md | 4 +- .../Agent_With_AzureOpenAIResponses.csproj | 3 +- .../Program.cs | 20 ++++-- .../Agent_With_AzureOpenAIResponses/README.md | 4 +- .../Program.cs | 3 +- .../README.md | 8 +-- .../Agent_Step07_SkillsAutoApproval.csproj | 1 - .../Program.cs | 10 ++- .../DeclarativeChatClientAgents.csproj | 3 +- .../DeclarativeAgents/ChatClient/Program.cs | 14 ++-- .../DevUI_Step01_BasicUsage.csproj | 1 - .../DevUI/DevUI_Step01_BasicUsage/Program.cs | 10 ++- .../Agent_MCP_LongRunningTask_Client.csproj | 3 +- .../Program.cs | 14 ++-- .../Agent_MCP_Server/Agent_MCP_Server.csproj | 3 +- .../Agent_MCP_Server/Program.cs | 14 ++-- .../Agent_MCP_Server_Auth.csproj | 3 +- .../Agent_MCP_Server_Auth/Program.cs | 14 ++-- .../Agent_MCP_Server_Auth/README.md | 27 +++++-- .../ResponseAgent_Hosted_MCP/Program.cs | 20 ++++-- .../ResponseAgent_Hosted_MCP.csproj | 3 +- .../Concurrent/MapReduce/MapReduce.csproj | 3 +- .../HostedWorkflowHandoff.csproj | 5 +- .../Hosted-Workflow-Handoff/Program.cs | 17 +++-- .../Hosted-Workflow-Handoff/README.md | 2 +- .../AGUIDojoServer/AGUIDojoServer.csproj | 3 +- .../AGUIDojoServer/ChatClientAgentFactory.cs | 16 +++-- .../AGUIServer/AGUIServer.csproj | 2 +- .../AGUIClientServer/AGUIServer/Program.cs | 12 ++-- .../05-end-to-end/AGUIClientServer/README.md | 13 +++- .../05-end-to-end/AGUIWebChat/README.md | 10 +-- .../Server/AGUIWebChatServer.csproj | 3 +- .../AGUIWebChat/Server/Program.cs | 14 ++-- .../AgentWithPurview/AgentWithPurview.csproj | 1 - .../05-end-to-end/AgentWithPurview/Program.cs | 14 ++-- .../05-end-to-end/AgentWithPurview/README.md | 58 +++++++++++++++ .../05-end-to-end/M365Agent/M365Agent.csproj | 1 - .../05-end-to-end/M365Agent/Program.cs | 13 ++-- .../src/Microsoft.Agents.AI.Purview/README.md | 42 +++++++---- ...crosoft.Agents.AI.Foundry.UnitTests.csproj | 1 - .../PolicyPipelineInvestigationTests.cs | 20 +++--- ...ng.AGUI.AspNetCore.IntegrationTests.csproj | 3 +- ...icrosoft.Agents.AI.OpenAI.UnitTests.csproj | 4 -- .../UserAgentPolicyTests.cs | 21 +++--- 61 files changed, 433 insertions(+), 220 deletions(-) create mode 100644 dotnet/samples/05-end-to-end/AgentWithPurview/README.md diff --git a/.github/upgrades/prompts/SemanticKernelToAgentFramework.md b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md index f5ef878fea1..922ea46f805 100644 --- a/.github/upgrades/prompts/SemanticKernelToAgentFramework.md +++ b/.github/upgrades/prompts/SemanticKernelToAgentFramework.md @@ -221,8 +221,8 @@ using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.Extensions.AI; using Microsoft.Agents.AI; // Provider-specific namespaces (add only if needed): -using OpenAI; // For OpenAI provider -using Azure.AI.OpenAI; // For Azure OpenAI provider +using OpenAI; // For OpenAI and Azure OpenAI providers +using System.ClientModel.Primitives; // For BearerTokenPolicy with Azure OpenAI using Azure.AI.Agents.Persistent; // For Microsoft Foundry provider using Azure.Identity; // For Azure authentication ``` @@ -545,9 +545,13 @@ AIAgent agent = new OpenAIClient(apiKey) **Azure OpenAI:** ```csharp -AIAgent agent = new AzureOpenAIClient(endpoint, credential) +Uri openAIEndpoint = new($"{endpoint.ToString().TrimEnd('/')}/openai/v1/"); + +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(credential, "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = openAIEndpoint }) .GetChatClient(deploymentName) - .CreateAIAgent(instructions: instructions); + .AsAIAgent(instructions: instructions); ``` **Microsoft Foundry (New):** @@ -571,9 +575,13 @@ AIAgent agent = new OpenAIClient(apiKey) **Azure OpenAI Responses:** *(Recommended for Azure OpenAI)* ```csharp -AIAgent agent = new AzureOpenAIClient(endpoint, credential) - .GetOpenAIResponseClient(deploymentName) - .CreateAIAgent(instructions: instructions); +Uri openAIEndpoint = new($"{endpoint.ToString().TrimEnd('/')}/openai/v1/"); + +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(credential, "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = openAIEndpoint }) + .GetResponsesClient() + .AsAIAgent(model: deploymentName, instructions: instructions); ``` **A2A:** @@ -979,11 +987,11 @@ AgentThread thread = agent.GetNewThread(); **Add Agent Framework Packages:** ```xml - + ``` -**Note**: If not using `AzureCliCredential`, you can use `ApiKeyCredential` instead without the `Azure.Identity` package. +**Note**: If not using Entra ID (`AzureCliCredential` / `DefaultAzureCredential`), you can use `ApiKeyCredential` instead without the `Azure.Identity` package. **Before (Semantic Kernel):** @@ -1005,13 +1013,18 @@ ChatCompletionAgent agent = new() **After (Agent Framework):** ```csharp -using Microsoft.Agents.AI; -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; +using Microsoft.Agents.AI; +using OpenAI; +using OpenAI.Chat; -AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) +var openAIEndpoint = $"{endpoint.TrimEnd('/')}/openai/v1/"; +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(openAIEndpoint) }) .GetChatClient(deploymentName) - .CreateAIAgent(instructions: "You are a helpful assistant"); + .AsAIAgent(instructions: "You are a helpful assistant"); ``` ### 3. OpenAI Assistants Migration @@ -1273,14 +1286,15 @@ var result = await agent.RunAsync(userInput, thread); **Add Agent Framework Packages:** ```xml - + + ``` **Replace this Semantic Kernel pattern:** -Azure OpenAI Responses uses `AzureOpenAIClient` instead of `OpenAIClient`. The thread management is done manually where the thread needs to be passed to the `InvokeAsync` method and updated with the `item.Thread` from the response. +Azure OpenAI Responses uses the OpenAI SDK with a custom endpoint and Entra token policy. The thread management is done manually where the thread needs to be passed to the `InvokeAsync` method and updated with the `item.Thread` from the response. ```csharp using Microsoft.SemanticKernel.Agents.OpenAI; @@ -1311,21 +1325,28 @@ await foreach (AgentResponseItem responseItem in responseIte Agent Framework automatically manages the thread, so there's no need to manually update it. ```csharp -using Microsoft.Agents.AI.OpenAI; -using Azure.AI.OpenAI; - -AIAgent agent = new AzureOpenAIClient(endpoint, new AzureCliCredential()) - .GetOpenAIResponseClient(modelId) - .CreateAIAgent( +using System.ClientModel.Primitives; +using Azure.Identity; +using Microsoft.Agents.AI; +using OpenAI; +using OpenAI.Responses; + +Uri openAIEndpoint = new($"{endpoint.ToString().TrimEnd('/')}/openai/v1/"); +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = openAIEndpoint }) + .GetResponsesClient() + .AsAIAgent( + model: modelId, name: "ResponseAgent", instructions: "Answer all queries in English and French.", tools: [/* AITools */]); -AgentThread thread = agent.GetNewThread(); +AgentSession session = await agent.CreateSessionAsync(); -var result = await agent.RunAsync(userInput, thread); +var result = await agent.RunAsync(userInput, session); -// The thread will be automatically updated with the new response id from this point +// The session is updated with the new response id. ``` @@ -1608,4 +1629,3 @@ var filteredAgent = originalAgent .Build(); ``` - diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index f75a3f2dd38..50cc410e6f9 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -1,4 +1,4 @@ - + @@ -29,7 +29,6 @@ - diff --git a/dotnet/README.md b/dotnet/README.md index 328dfdf6840..28799111510 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -5,17 +5,21 @@ ### Basic Agent - .NET ```c# -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; +using OpenAI; using OpenAI.Responses; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!; +// Use the Azure OpenAI v1 route with the OpenAI SDK (resource root + /openai/v1). +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!; // e.g. https://YOUR.openai.azure.com/openai/v1/ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")!; -var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) - .GetResponsesClient(deploymentName) - .AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully."); +var agent = new OpenAIClient( + new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + .GetResponsesClient() + .AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully."); Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework.")); ``` diff --git a/dotnet/samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj b/dotnet/samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj index d91b20e34bc..3e4914ac010 100644 --- a/dotnet/samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj +++ b/dotnet/samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj @@ -10,7 +10,6 @@ - diff --git a/dotnet/samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj b/dotnet/samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj index e75368ea992..3e4914ac010 100644 --- a/dotnet/samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj +++ b/dotnet/samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj @@ -1,4 +1,4 @@ - + Exe @@ -10,7 +10,6 @@ - diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs index 53eaa24981d..0f342ccef58 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs @@ -1,9 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; +using OpenAI; using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -18,6 +19,11 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -25,9 +31,9 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -ChatClient chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +ChatClient chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName); AIAgent agent = chatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj index a551fed5120..58e9b3a240b 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Server.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,7 +8,6 @@ - diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs index 653ad20b163..e671f344344 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs @@ -1,13 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using System.ComponentModel; using System.Text.Json.Serialization; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; using Microsoft.Extensions.Options; +using OpenAI; using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -24,6 +25,11 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -82,9 +88,9 @@ static RestaurantSearchResponse SearchRestaurants( // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -ChatClient chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +ChatClient chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName); ChatClientAgent agent = chatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj index a551fed5120..58e9b3a240b 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Server.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,7 +8,6 @@ - diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs index 53eaa24981d..0f342ccef58 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs @@ -1,9 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; +using OpenAI; using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -18,6 +19,11 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -25,9 +31,9 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -ChatClient chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +ChatClient chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName); AIAgent agent = chatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj index a551fed5120..58e9b3a240b 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Server.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,7 +8,6 @@ - diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs index 09766857083..17bbcd012df 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using System.ComponentModel; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; +using OpenAI; using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -20,6 +21,11 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -41,9 +47,9 @@ static string ApproveExpenseReport(string expenseReportId) // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -ChatClient openAIChatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +ChatClient openAIChatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName); ChatClientAgent baseAgent = openAIChatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj index a551fed5120..58e9b3a240b 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Server.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,7 +8,6 @@ - diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs index 4e255beb548..9481d6c2bfc 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using System.ComponentModel; using AGUI.Server; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; using Microsoft.Extensions.AI; +using OpenAI; using OpenAI.Chat; using RecipeAssistant; @@ -27,6 +28,11 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -59,9 +65,9 @@ full list of ingredients (each with an icon, name and amount) and the step-by-st // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -ChatClient chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +ChatClient chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName); AIAgent baseAgent = chatClient.AsAIAgent(new ChatClientAgentOptions diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj index a551fed5120..58e9b3a240b 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Server.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,7 +8,6 @@ - diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj index 54cc460fe9d..addddccc96c 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs index 024adf626d2..a1de47e8e79 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs @@ -2,20 +2,26 @@ // This sample shows how to create and use a simple AI agent with Azure OpenAI Chat Completion as the backend. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; +using OpenAI; using OpenAI.Chat; var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName) .AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker"); diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/README.md b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/README.md index 2c22cd623ee..16320e25b00 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/README.md +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/README.md @@ -11,6 +11,8 @@ Before you begin, ensure you have the following prerequisites: Set the following environment variables: ```powershell -$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint +# Resource root is fine (sample appends /openai/v1). You can also set the full v1 endpoint. +$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" +# or: $env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/openai/v1/" $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini ``` diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj index 54cc460fe9d..addddccc96c 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Agent_With_AzureOpenAIResponses.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs index 53f036b279f..33235c349d7 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs @@ -2,22 +2,28 @@ // This sample shows how to create and use a simple AI agent with Azure OpenAI Responses as the backend. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using OpenAI; using OpenAI.Responses; var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. // You must dissable client side conversation storage for clients that support it -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "Joker"); @@ -27,9 +33,9 @@ // Create a responses based agent with "store"=false. // This means that chat history is managed locally by Agent Framework // instead of being stored in the service (default). -AIAgent agentStoreFalse = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agentStoreFalse = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsIChatClientWithStoredOutputDisabled(model: deploymentName) .AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker"); diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/README.md b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/README.md index 2c22cd623ee..16320e25b00 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/README.md +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/README.md @@ -11,6 +11,8 @@ Before you begin, ensure you have the following prerequisites: Set the following environment variables: ```powershell -$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint +# Resource root is fine (sample appends /openai/v1). You can also set the full v1 endpoint. +$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" +# or: $env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/openai/v1/" $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini ``` diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs index 3d33c91c9d0..2cf89949231 100644 --- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs +++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/Program.cs @@ -38,8 +38,7 @@ } // Extract container file citations from response annotations and download. -// AIProjectClient.GetProjectOpenAIClient() returns a ProjectOpenAIClient (inherits from OpenAI.OpenAIClient) -// which supports GetContainerClient(), unlike AzureOpenAIClient which does not. +// AIProjectClient.GetProjectOpenAIClient() returns a ProjectOpenAIClient that supports GetContainerClient(). var containerClient = aiProjectClient.GetProjectOpenAIClient().GetContainerClient(); HashSet downloadedFiles = []; diff --git a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/README.md b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/README.md index 8526a628750..a07d0e3ba37 100644 --- a/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/README.md +++ b/dotnet/samples/02-agents/AgentProviders/foundry/Agent_Step24_CodeInterpreterFileDownload/README.md @@ -17,14 +17,10 @@ These container files **cannot** be downloaded using the standard Files API (`Ge ### Getting the ContainerClient with Foundry -`AzureOpenAIClient.GetContainerClient()` is not supported and throws `InvalidOperationException`. Instead, use the project's OpenAI client which inherits directly from `OpenAI.OpenAIClient`: +Use the Foundry project OpenAI client from `AIProjectClient` to access the Containers API: ```csharp -// ❌ AzureOpenAIClient does not support ContainerClient -var azureClient = new AzureOpenAIClient(endpoint, credential); -azureClient.GetContainerClient(); // Throws InvalidOperationException - -// ✅ Use AIProjectClient's project OpenAI client +// Use AIProjectClient's project OpenAI client var containerClient = aiProjectClient.GetProjectOpenAIClient().GetContainerClient(); await containerClient.DownloadContainerFileAsync("cntr_...", "cfile_..."); ``` diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Agent_Step07_SkillsAutoApproval.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Agent_Step07_SkillsAutoApproval.csproj index 4da23d66392..5838955fb8b 100644 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Agent_Step07_SkillsAutoApproval.csproj +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Agent_Step07_SkillsAutoApproval.csproj @@ -10,7 +10,6 @@ - diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs index a68ea077bb3..a25c2f5b798 100644 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs @@ -9,14 +9,20 @@ // All tools exposed by AgentSkillsProvider always require approval by default. // Auto-approval rules let you selectively bypass the approval prompt for safe operations. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using OpenAI; using OpenAI.Responses; // --- Configuration --- string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // --- Skills Provider --- @@ -30,7 +36,7 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) +AIAgent agent = new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsAIAgent(new ChatClientAgentOptions { diff --git a/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj b/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj index 157b70b0521..d72e9b73e29 100644 --- a/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj +++ b/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs b/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs index 78634eb62db..dd9d60030db 100644 --- a/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs +++ b/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs @@ -2,22 +2,28 @@ // This sample shows how to load an AI agent from a YAML file and process a prompt using Azure OpenAI as the backend. +using System.ClientModel.Primitives; using System.ComponentModel; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using OpenAI; var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // Create the chat client // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -IChatClient chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +IChatClient chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName) .AsIChatClient(); diff --git a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj index 09037b5f1d7..4b20e796f1b 100644 --- a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj +++ b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/DevUI_Step01_BasicUsage.csproj @@ -16,7 +16,6 @@ - diff --git a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs index ca3e38ad243..cc6c5bd804f 100644 --- a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs +++ b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs @@ -2,14 +2,15 @@ // This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents. +using System.ClientModel.Primitives; using System.ComponentModel; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.DevUI; using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; +using OpenAI; namespace DevUI_Step01_BasicUsage; @@ -44,12 +45,17 @@ private static void Main(string[] args) // Set up the Azure OpenAI client var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); + // OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. + if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) + { + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; + } var deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? "gpt-5.4-mini"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. - var chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) + var chatClient = new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName) .AsIChatClient(); diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj index fb36ab7e167..7fa596ea827 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj @@ -1,4 +1,4 @@ - + Exe @@ -10,7 +10,6 @@ - diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs index 99ce16242ce..b234ca52fcf 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs @@ -20,8 +20,8 @@ // // No application-level loop or continuation tokens are required in either mode. +using System.ClientModel.Primitives; using System.ComponentModel; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Mcp; @@ -33,6 +33,7 @@ using ModelContextProtocol.Extensions.Tasks; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; +using OpenAI; using OpenAI.Chat; if (args.Length > 0 && args[0] == "--server") @@ -42,6 +43,11 @@ } var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // Launch this same assembly as a stdio MCP server in a child process. @@ -60,9 +66,9 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName) .AsAIAgent( instructions: "You answer data-analysis questions by invoking the available tools. Always invoke a tool when one matches the request.", diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj index aa73860c141..45da5bafbdd 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs index d1e80b65df2..fd1c8cddb1a 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs @@ -2,14 +2,20 @@ // This sample shows how to create and use a simple AI agent with tools from an MCP Server. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using ModelContextProtocol.Client; +using OpenAI; using OpenAI.Chat; var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // Create an MCPClient for the GitHub server @@ -26,9 +32,9 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName) .AsAIAgent(instructions: "You answer questions related to GitHub repositories only.", tools: [.. mcpTools.Cast()]); diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj index 46c13061496..e5948553458 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs index 362f5913708..ac5f18fee60 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs @@ -2,19 +2,25 @@ // This sample shows how to create and use a simple AI agent with tools from an MCP Server that requires authentication. +using System.ClientModel.Primitives; using System.Diagnostics; using System.Net; using System.Text; using System.Web; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.Logging; using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; +using OpenAI; using OpenAI.Chat; var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // We can customize a shared HttpClient with a custom handler if desired @@ -53,9 +59,9 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName) .AsAIAgent(instructions: "You answer questions related to the weather.", tools: [.. mcpTools]); diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/README.md b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/README.md index 59c0af0c3f3..20eb14c478d 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/README.md +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/README.md @@ -20,16 +20,30 @@ The sample shows: - .NET 10.0 or later - A running TestOAuthServer (for OAuth authentication), see [Start the Test OAuth Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpClient#step-1-start-the-test-oauth-server) - A running ProtectedMCPServer (for MCP services), see [Start the Protected MCP Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpClient#step-2-start-the-protected-mcp-server) - + +Clone the MCP .NET SDK if it is not already available locally: + +```powershell +git clone https://github.com/modelcontextprotocol/csharp-sdk.git +dotnet dev-certs https --trust +``` + ## Configuring Environment Variables Set the following environment variables: ```powershell -$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint -$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini +# Azure OpenAI resource root (the sample appends /openai/v1 automatically): +$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" +$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" + +# A Foundry project OpenAI v1 endpoint also works: +# $env:AZURE_OPENAI_ENDPOINT="https://your-resource.services.ai.azure.com/api/projects/your-project/openai/v1/" ``` +The OpenAI-compatible model is separate from MCP OAuth. MCP OAuth authenticates access to the weather +tools. The model chooses when to call those tools. + ## Setup and Running ### Step 1: Start the Test OAuth Server @@ -100,12 +114,15 @@ sequenceDiagram ## OAuth Configuration The client is configured with: -- **Client ID**: `demo-client` -- **Client Secret**: `demo-secret` +- **Client registration**: Dynamic Client Registration performed automatically by the MCP client +- **Client name**: `ProtectedMcpClient` - **Redirect URI**: `http://localhost:1179/callback` - **OAuth Server**: `https://localhost:7029` - **Protected Resource**: `http://localhost:7071` +The test authorization endpoint immediately redirects back with an authorization code. No test username, +password, client ID, or client secret needs to be entered. + ## Available Tools Once authenticated, the client can access weather tools including: diff --git a/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs index 59d47d57b1a..96ff647e9ed 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs @@ -4,13 +4,19 @@ // In this case the OpenAI responses service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework. // The sample first shows how to use MCP tools with auto approval, and then how to set up a tool that requires approval before it can be invoked and how to approve such a tool. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using OpenAI; using OpenAI.Responses; var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // **** MCP Tool with Auto Approval **** @@ -30,9 +36,9 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsAIAgent( model: deploymentName, @@ -58,9 +64,9 @@ }; // Create an agent based on Azure OpenAI Responses as the backend. -AIAgent agentWithRequiredApproval = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +AIAgent agentWithRequiredApproval = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsAIAgent( model: deploymentName, diff --git a/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj b/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj index 41aafe34372..34f8c30f0cb 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj +++ b/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/03-workflows/Concurrent/MapReduce/MapReduce.csproj b/dotnet/samples/03-workflows/Concurrent/MapReduce/MapReduce.csproj index 21a7f8c176f..17756b4c807 100644 --- a/dotnet/samples/03-workflows/Concurrent/MapReduce/MapReduce.csproj +++ b/dotnet/samples/03-workflows/Concurrent/MapReduce/MapReduce.csproj @@ -1,4 +1,4 @@ - + Exe @@ -12,7 +12,6 @@ - diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj index aeb5cd12f75..9bdcf4161b6 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj @@ -23,10 +23,11 @@ - - + + + diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs index d9370e83f6c..1320c843324 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs @@ -15,8 +15,8 @@ // - AZURE_OPENAI_ENDPOINT - your Azure OpenAI endpoint // - AZURE_OPENAI_DEPLOYMENT - the model deployment name (default: "gpt-4o") +using System.ClientModel.Primitives; using System.ComponentModel; -using Azure.AI.OpenAI; using Azure.Identity; using DotNetEnv; using Microsoft.Agents.AI; @@ -25,6 +25,7 @@ using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using ModelContextProtocol.Client; +using OpenAI; // Load .env file if present (for local development) Env.TraversePath().Load(); @@ -34,13 +35,21 @@ // --------------------------------------------------------------------------- // 1. Create the shared Azure OpenAI chat client // --------------------------------------------------------------------------- -var endpoint = new Uri(System.Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.")); -var deployment = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o"; +var endpointValue = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpointValue).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpointValue = $"{endpointValue.TrimEnd('/')}/openai/v1"; +} +var endpoint = new Uri(endpointValue); +var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -var azureClient = new AzureOpenAIClient(endpoint, new DefaultAzureCredential()); +var azureClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = endpoint }); IChatClient chatClient = azureClient.GetResponsesClient().AsIChatClient(deployment); // --------------------------------------------------------------------------- diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md index f04665e17b8..1c67015fe07 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md @@ -10,7 +10,7 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the > deploying. > **The deployed agent needs its own role on that Azure OpenAI resource.** Because the workflow -> builds its own `AzureOpenAIClient` (a data-plane client) instead of using the Foundry project's +> builds its own `OpenAIClient` targeting the Azure OpenAI v1 endpoint instead of using the Foundry project's > hosted model, `azd deploy` does **not** grant it access automatically. `azd` only grants the agent > identity the `Foundry User` role on the project; it does not touch a separate Azure OpenAI account. > After the first deploy, grant the agent's managed identity the **`Cognitive Services OpenAI User`** diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj index 03e2493623d..3a9b9248c4a 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs index 1cdd00731bb..3f5e4492822 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs @@ -1,35 +1,41 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using System.ComponentModel; using System.Text.Json; using AGUIDojoServer.AgenticUI; using AGUIDojoServer.BackendToolRendering; using AGUIDojoServer.PredictiveStateUpdates; using AGUIDojoServer.SharedState; -using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; +using OpenAI; using OpenAI.Chat; namespace AGUIDojoServer; internal static class ChatClientAgentFactory { - private static AzureOpenAIClient? s_azureOpenAIClient; + private static OpenAIClient? s_azureOpenAIClient; private static string? s_deploymentName; public static void Initialize(IConfiguration configuration) { string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); + // OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. + if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) + { + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; + } s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. - s_azureOpenAIClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()); + s_azureOpenAIClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }); } public static ChatClientAgent CreateAgenticChat() diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj index 788f6a3f3ff..ebea1ee422b 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index 6f1ed5ba3ef..9a15e144a15 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -16,6 +16,11 @@ builder.Services.AddAGUIServer(); string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); const string AgentName = "AGUIAssistant"; @@ -30,9 +35,8 @@ .GetResponsesClient() .AsIChatClientWithStoredOutputDisabled(model: deploymentName); -// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, -// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user -// deployments, e.g.: +// This local sample accepts anonymous requests, so it disables per-user session isolation below. +// In production, authenticate callers and register an AgentIsolationKeyProvider, e.g.: // builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); // Register the agent with the host and configure it to use an in-memory session store @@ -58,7 +62,7 @@ name: "get_server_weather_forecast", description: "Gets the forecast for a specific location and date", AGUIServerSerializerContext.Default.Options)) - .WithInMemorySessionStore(); + .WithInMemorySessionStore(withIsolation: false); WebApplication app = builder.Build(); diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md index 30adeb1eef2..91df6c4daa5 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md @@ -43,6 +43,17 @@ dotnet run --urls "http://localhost:5100" The server will start and listen on `http://localhost:5100`. +The local server accepts anonymous requests and uses: + +```csharp +.WithInMemorySessionStore(withIsolation: false); +``` + +`WithInMemorySessionStore()` enables per-user isolation by default. That default requires an +`AgentIsolationKeyProvider`, normally backed by the authenticated caller's claims. This anonymous +local sample has no user identity, so it disables isolation explicitly. In production, authenticate +callers and register `UseClaimsBasedAgentIsolation(...)` instead. + ### Step 2: Testing with the REST Client (Optional) Before running the client, you can test the server using the included `.http` file: @@ -133,7 +144,7 @@ IChatClient chatClient = new OpenAIClient( builder .AddAIAgent("AGUIAssistant", "You are a helpful assistant.", chatClient) .WithAITool(new HostedWebSearchTool()) - .WithInMemorySessionStore(); + .WithInMemorySessionStore(withIsolation: false); app.MapAGUIServer("AGUIAssistant", "/"); ``` diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/README.md b/dotnet/samples/05-end-to-end/AGUIWebChat/README.md index 78e30803115..728a0c8d68e 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/README.md +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/README.md @@ -66,12 +66,12 @@ Features: The server (`Server/Program.cs`) creates a simple chat agent: ```csharp -// Create Azure OpenAI client -AzureOpenAIClient azureOpenAIClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()); +// Create OpenAI client targeting Azure OpenAI +OpenAIClient openAIClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }); -ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName); +ChatClient chatClient = openAIClient.GetChatClient(deploymentName); // Create AI agent ChatClientAgent agent = chatClient.AsAIAgent( diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj index 267ab5e4158..2afaa01d239 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/AGUIWebChatServer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -8,7 +8,6 @@ - diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs index 6f888b9dc36..da8486064ee 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs @@ -2,10 +2,11 @@ // This sample demonstrates a basic AG-UI server hosting a chat agent for the Blazor web client. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore; +using OpenAI; using OpenAI.Chat; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -20,15 +21,20 @@ WebApplication app = builder.Build(); string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); // Create the AI agent // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AzureOpenAIClient azureOpenAIClient = new( - new Uri(endpoint), - new DefaultAzureCredential()); +OpenAIClient azureOpenAIClient = new( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }); ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName); diff --git a/dotnet/samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj b/dotnet/samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj index 0a79857d64d..f37e9fc7361 100644 --- a/dotnet/samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj +++ b/dotnet/samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj @@ -9,7 +9,6 @@ - diff --git a/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs b/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs index aadb48c6352..e05f02c9b2b 100644 --- a/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs +++ b/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs @@ -5,14 +5,20 @@ // Authentication to Purview is done using an InteractiveBrowserCredential. // Any TokenCredential with Purview API permissions can be used here. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Core; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Purview; using Microsoft.Extensions.AI; +using OpenAI; var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; var purviewClientAppId = Environment.GetEnvironmentVariable("PURVIEW_CLIENT_APP_ID") ?? throw new InvalidOperationException("PURVIEW_CLIENT_APP_ID is not set."); @@ -27,9 +33,9 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -using IChatClient client = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) +using IChatClient client = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetResponsesClient() .AsIChatClient(deploymentName) .AsBuilder() diff --git a/dotnet/samples/05-end-to-end/AgentWithPurview/README.md b/dotnet/samples/05-end-to-end/AgentWithPurview/README.md new file mode 100644 index 00000000000..a116a8e09bc --- /dev/null +++ b/dotnet/samples/05-end-to-end/AgentWithPurview/README.md @@ -0,0 +1,58 @@ +# Agent with Purview + +This sample adds Microsoft Purview policy evaluation to an OpenAI-compatible chat client. + +## What is required + +The model endpoint and Purview authentication are separate: + +- The model endpoint generates the response. +- Microsoft Graph Purview APIs evaluate the prompt and response against tenant policies. + +Set the model configuration: + +```powershell +# Azure OpenAI resource root (the sample appends /openai/v1 automatically): +$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" +$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" + +# A Foundry project OpenAI v1 endpoint also works: +# $env:AZURE_OPENAI_ENDPOINT="https://your-resource.services.ai.azure.com/api/projects/your-project/openai/v1/" +``` + +Create or reuse a Microsoft Entra public client application and set: + +```powershell +$env:PURVIEW_CLIENT_APP_ID="" +``` + +The app requires these delegated Microsoft Graph permissions with tenant administrator consent: + +- `ProtectionScopes.Compute.All` +- `Content.Process.All` +- `ContentActivity.Write` + +Configure a localhost redirect URI for the public client application so `InteractiveBrowserCredential` +can complete sign-in. + +## Tenant configuration + +A successful authentication only proves that the Graph permissions are configured. A real Purview block +also requires: + +1. Microsoft Purview entitlement and consumptive billing. +2. The Entra app registered in **Purview > Settings > AI app and agent locations**. +3. A DLP or data collection policy targeting the app and signed-in user. +4. The policy enabled outside test-only mode. + +## Run + +```powershell +az login +dotnet run +``` + +The sample opens a browser for the delegated Purview sign-in, then prompts for text to send to the model. + +For middleware options and policy behavior, see +[`Microsoft.Agents.AI.Purview`](../../../src/Microsoft.Agents.AI.Purview/README.md). diff --git a/dotnet/samples/05-end-to-end/M365Agent/M365Agent.csproj b/dotnet/samples/05-end-to-end/M365Agent/M365Agent.csproj index 72352b7f018..463f0638555 100644 --- a/dotnet/samples/05-end-to-end/M365Agent/M365Agent.csproj +++ b/dotnet/samples/05-end-to-end/M365Agent/M365Agent.csproj @@ -16,7 +16,6 @@ - diff --git a/dotnet/samples/05-end-to-end/M365Agent/Program.cs b/dotnet/samples/05-end-to-end/M365Agent/Program.cs index 6e4bc0c0b46..ad33dabb02c 100644 --- a/dotnet/samples/05-end-to-end/M365Agent/Program.cs +++ b/dotnet/samples/05-end-to-end/M365Agent/Program.cs @@ -4,7 +4,7 @@ // The agent can then be consumed from various M365 channels. // See the README.md for more information. -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Identity; using M365Agent; using M365Agent.Agents; @@ -35,13 +35,18 @@ { var deploymentName = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("DeploymentName")!; var endpoint = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("Endpoint")!; + // OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. + if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) + { + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; + } // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. - chatClient = new AzureOpenAIClient( - new Uri(endpoint), - new DefaultAzureCredential()) + chatClient = new OpenAIClient( + new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) .GetChatClient(deploymentName) .AsIChatClient(); } diff --git a/dotnet/src/Microsoft.Agents.AI.Purview/README.md b/dotnet/src/Microsoft.Agents.AI.Purview/README.md index 28039b5dd31..39915507b25 100644 --- a/dotnet/src/Microsoft.Agents.AI.Purview/README.md +++ b/dotnet/src/Microsoft.Agents.AI.Purview/README.md @@ -28,14 +28,15 @@ Add Purview when you need to: ## Quick Start ``` csharp -using Azure.AI.OpenAI; +using System.ClientModel.Primitives; using Azure.Core; using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Purview; using Microsoft.Extensions.AI; +using OpenAI; -Uri endpoint = new Uri("..."); // The endpoint of Azure OpenAI instance. +Uri endpoint = new Uri("https://your-resource.openai.azure.com/openai/v1/"); string deploymentName = "..."; // The deployment name of your Azure OpenAI instance ex: gpt-4o-mini string purviewClientAppId = "..."; // The client id of your entra app registration. @@ -47,11 +48,11 @@ TokenCredential browserCredential = new InteractiveBrowserCredential( ClientId = purviewClientAppId }); -IChatClient client = new AzureOpenAIClient( - new Uri(endpoint), - new AzureCliCredential()) - .GetResponsesClient(deploymentName) - .AsIChatClient() +IChatClient client = new OpenAIClient( + new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = endpoint }) + .GetResponsesClient() + .AsIChatClient(deploymentName) .AsBuilder() .WithPurview(browserCredential, new PurviewSettings("My Sample App")) .Build(); @@ -82,6 +83,17 @@ The plugin requires the following Graph permissions: - Content.Process.All : [processContent](https://learn.microsoft.com/en-us/graph/api/userdatasecurityandgovernance-processcontent) - ContentActivity.Write : [contentActivity](https://learn.microsoft.com/en-us/graph/api/activitiescontainer-post-contentactivities) +The Entra app must be configured as a public client application with a localhost redirect URI so +`InteractiveBrowserCredential` can complete the delegated user sign-in. These permissions require tenant +administrator consent. + +The client ID alone is not enough for a live Purview policy test. The tenant must also: + +1. Have Microsoft Purview entitlement and consumptive billing enabled. +2. Register the Entra app as an integrated AI app in **Purview > Settings > AI app and agent locations**. +3. Configure a DLP or data collection policy that applies to the signed-in user and the app. +4. Turn the policy on. A policy in test mode does not produce an enforced block. + Authentication with user tokens is preferred. When the configured credential resolves to a user token, that token's user id is used for Purview policy evaluation. If authenticating with app tokens, the token does not contain an end-user principal, so the agent-framework caller will need to provide an entra user id for each `ChatMessage` sent to the agent/client. This user id can be set using the `SetUserId` extension method, or by setting the `"userId"` field of the `AdditionalProperties` dictionary. ``` csharp @@ -182,9 +194,9 @@ var settings = new PurviewSettings("My Sample App") Use the agent middleware when you already have / want the full agent pipeline: ``` csharp -AIAgent agent = new AzureOpenAIClient( - new Uri(endpoint), - new AzureCliCredential()) +AIAgent agent = new OpenAIClient( + new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsAIAgent("You are a helpful assistant.") .AsBuilder() @@ -195,11 +207,11 @@ AIAgent agent = new AzureOpenAIClient( Use the chat middleware when you attach directly to a chat client (e.g. minimal agent shell or custom orchestration): ``` csharp -IChatClient client = new AzureOpenAIClient( - new Uri(endpoint), - new AzureCliCredential()) - .GetResponsesClient(deploymentName) - .AsIChatClient() +IChatClient client = new OpenAIClient( + new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"), + new OpenAIClientOptions { Endpoint = endpoint }) + .GetResponsesClient() + .AsIChatClient(deploymentName) .AsBuilder() .WithPurview(browserCredential, new PurviewSettings("Agent Framework Test App")) .Build(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 5418c8b49e0..05b94b7b82f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -8,7 +8,6 @@ - diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/PolicyPipelineInvestigationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/PolicyPipelineInvestigationTests.cs index f1fcc12a924..3c4fdd45cb2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/PolicyPipelineInvestigationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/PolicyPipelineInvestigationTests.cs @@ -10,8 +10,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.AI; +using OpenAI; using OpenAI.Chat; using OpenAI.Responses; @@ -88,9 +88,9 @@ protected override Task SendAsync(HttpRequestMessage reques } /// -/// Executable probes for caller-owned Azure OpenAI clients wrapped by Microsoft.Extensions.AI. +/// Executable probes for caller-owned OpenAI clients targeting Azure OpenAI endpoints and wrapped by Microsoft.Extensions.AI. /// -public sealed class AzureOpenAIRequestPoliciesInvestigationTests +public sealed class OpenAIRequestPoliciesInvestigationTests { [Fact] public async Task CallerOwnedChatAndResponsesWrappers_HaveIsolatedPolicies_PreserveTransport_AndRunAfterBaseUserAgentAsync() @@ -100,14 +100,12 @@ public async Task CallerOwnedChatAndResponsesWrappers_HaveIsolatedPolicies_Prese #pragma warning disable CA5399 using var httpClient = new HttpClient(handler, disposeHandler: false); #pragma warning restore CA5399 - var options = new AzureOpenAIClientOptions + var options = new OpenAIClientOptions { + Endpoint = new Uri("https://resource.openai.azure.com/openai/v1/"), Transport = new HttpClientPipelineTransport(httpClient), }; - var azureClient = new AzureOpenAIClient( - new Uri("https://resource.openai.azure.com/"), - new ApiKeyCredential("test-key"), - options); + var azureClient = new OpenAIClient(new ApiKeyCredential("test-key"), options); ChatClient callerOwnedChatClient = azureClient.GetChatClient("deployment"); ResponsesClient callerOwnedResponsesClient = azureClient.GetResponsesClient(); @@ -134,7 +132,6 @@ public async Task CallerOwnedChatAndResponsesWrappers_HaveIsolatedPolicies_Prese Assert.Contains(handler.Requests, static request => request.Marker == "chat-only"); Assert.Equal(2, handler.Requests.Count(static request => request.Marker is null)); Assert.Single(probe.ObservedUserAgents); - Assert.Contains("azsdk-net-AI.OpenAI/", probe.ObservedUserAgents[0]); Assert.Contains("MEAI/", probe.ObservedUserAgents[0]); } @@ -168,11 +165,12 @@ public async Task CallerOwnedAzureClient_PreservesActualAzureOpenAIAndLookalikeO private static IChatClient CreateChatWrapper(Uri endpoint, HttpClient httpClient) { - var options = new AzureOpenAIClientOptions + var options = new OpenAIClientOptions { + Endpoint = endpoint, Transport = new HttpClientPipelineTransport(httpClient), }; - return new AzureOpenAIClient(endpoint, new ApiKeyCredential("test-key"), options) + return new OpenAIClient(new ApiKeyCredential("test-key"), options) .GetChatClient("deployment") .AsIChatClient(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj index a65db7c2cf5..4862c270b8a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj @@ -1,4 +1,4 @@ - + $(TargetFrameworksCore) @@ -10,7 +10,6 @@ - diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj index f5d94683bc9..1fe15617d76 100644 --- a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj @@ -4,10 +4,6 @@ - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/UserAgentPolicyTests.cs b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/UserAgentPolicyTests.cs index 4e9213d0e2c..98fe55a8d86 100644 --- a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/UserAgentPolicyTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/UserAgentPolicyTests.cs @@ -9,10 +9,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.AI; using OpenAI.Chat; using OpenAI.Responses; +using OpenAIClient = OpenAI.OpenAIClient; +using OpenAIClientOptions = OpenAI.OpenAIClientOptions; namespace Microsoft.Agents.AI.OpenAI.UnitTests; @@ -58,7 +59,7 @@ public async Task AzureOpenAIChatAgent_EmitsBaseAndFeatureUserAgentAsync() { // Arrange using var handler = new RecordingHandler(); - AzureOpenAIClient client = CreateAzureOpenAIClient(handler); + OpenAIClient client = CreateAzureOpenAIClient(handler); ChatClientAgent agent = client.GetChatClient("deployment").AsAIAgent(); FeatureUsageAssert.Reset(); @@ -74,7 +75,7 @@ public async Task AzureOpenAIResponsesAgent_EmitsBaseAndFeatureUserAgentAsync() { // Arrange using var handler = new RecordingHandler(); - AzureOpenAIClient client = CreateAzureOpenAIClient(handler); + OpenAIClient client = CreateAzureOpenAIClient(handler); ChatClientAgent agent = client.GetResponsesClient().AsAIAgent(model: "deployment"); FeatureUsageAssert.Reset(); @@ -95,7 +96,7 @@ public async Task AzureOpenAIChatAgent_DisabledMaskEmitsOnlyBaseUserAgentAsync() Environment.SetEnvironmentVariable(FeatureMaskDisabledEnvironmentVariable, "true"); FeatureUsageAssert.Reset(); using var handler = new RecordingHandler(); - AzureOpenAIClient client = CreateAzureOpenAIClient(handler); + OpenAIClient client = CreateAzureOpenAIClient(handler); ChatClientAgent agent = client.GetChatClient("deployment").AsAIAgent(); // Act @@ -126,12 +127,12 @@ public async Task IneligibleOpenAIChatAgent_DoesNotEmitAgentFrameworkUserAgentAs #pragma warning disable CA5399 var httpClient = new HttpClient(handler, disposeHandler: false); #pragma warning restore CA5399 - var options = new global::OpenAI.OpenAIClientOptions + var options = new OpenAIClientOptions { Endpoint = new Uri(endpoint), Transport = new HttpClientPipelineTransport(httpClient), }; - var client = new global::OpenAI.OpenAIClient(new ApiKeyCredential("test-key"), options); + var client = new OpenAIClient(new ApiKeyCredential("test-key"), options); ChatClientAgent agent = client.GetChatClient("model").AsAIAgent(); FeatureUsageAssert.Reset(); @@ -146,16 +147,16 @@ public async Task IneligibleOpenAIChatAgent_DoesNotEmitAgentFrameworkUserAgentAs public void Dispose() => FeatureUsageAssert.Reset(); - private static AzureOpenAIClient CreateAzureOpenAIClient(HttpMessageHandler handler) + private static OpenAIClient CreateAzureOpenAIClient(HttpMessageHandler handler) { #pragma warning disable CA5399 var httpClient = new HttpClient(handler, disposeHandler: false); #pragma warning restore CA5399 - return new AzureOpenAIClient( - new Uri("https://resource.openai.azure.com/"), + return new OpenAIClient( new ApiKeyCredential("test-key"), - new AzureOpenAIClientOptions + new OpenAIClientOptions { + Endpoint = new Uri("https://resource.openai.azure.com/openai/v1/"), Transport = new HttpClientPipelineTransport(httpClient), }); } From 9a504cf728d51b58989b6fc32581b03e22bdb53c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:58:34 +0100 Subject: [PATCH 2/3] .NET: Address PR review findings --- .../AGUIClientServer/AGUIServer/Program.cs | 7 ++++--- .../05-end-to-end/AGUIClientServer/README.md | 13 +------------ dotnet/samples/05-end-to-end/AGUIWebChat/README.md | 5 +++++ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index 9a15e144a15..53514b6ef38 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -35,8 +35,9 @@ .GetResponsesClient() .AsIChatClientWithStoredOutputDisabled(model: deploymentName); -// This local sample accepts anonymous requests, so it disables per-user session isolation below. -// In production, authenticate callers and register an AgentIsolationKeyProvider, e.g.: +// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production, +// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user +// deployments, e.g.: // builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier }); // Register the agent with the host and configure it to use an in-memory session store @@ -62,7 +63,7 @@ name: "get_server_weather_forecast", description: "Gets the forecast for a specific location and date", AGUIServerSerializerContext.Default.Options)) - .WithInMemorySessionStore(withIsolation: false); + .WithInMemorySessionStore(); WebApplication app = builder.Build(); diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md index 91df6c4daa5..30adeb1eef2 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/README.md +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/README.md @@ -43,17 +43,6 @@ dotnet run --urls "http://localhost:5100" The server will start and listen on `http://localhost:5100`. -The local server accepts anonymous requests and uses: - -```csharp -.WithInMemorySessionStore(withIsolation: false); -``` - -`WithInMemorySessionStore()` enables per-user isolation by default. That default requires an -`AgentIsolationKeyProvider`, normally backed by the authenticated caller's claims. This anonymous -local sample has no user identity, so it disables isolation explicitly. In production, authenticate -callers and register `UseClaimsBasedAgentIsolation(...)` instead. - ### Step 2: Testing with the REST Client (Optional) Before running the client, you can test the server using the included `.http` file: @@ -144,7 +133,7 @@ IChatClient chatClient = new OpenAIClient( builder .AddAIAgent("AGUIAssistant", "You are a helpful assistant.", chatClient) .WithAITool(new HostedWebSearchTool()) - .WithInMemorySessionStore(withIsolation: false); + .WithInMemorySessionStore(); app.MapAGUIServer("AGUIAssistant", "/"); ``` diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/README.md b/dotnet/samples/05-end-to-end/AGUIWebChat/README.md index 728a0c8d68e..17f34dbebd3 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/README.md +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/README.md @@ -67,6 +67,11 @@ The server (`Server/Program.cs`) creates a simple chat agent: ```csharp // Create OpenAI client targeting Azure OpenAI +if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) +{ + endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; +} + OpenAIClient openAIClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) }); From 1de00f4d46378bbcf116becb4ce56e45de0f1809 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:24:32 +0100 Subject: [PATCH 3/3] .NET: Share Azure OpenAI endpoint normalization --- .../Step01_GettingStarted/Server/Program.cs | 10 ++---- .../Step02_BackendTools/Server/Program.cs | 10 ++---- .../Step03_FrontendTools/Server/Program.cs | 10 ++---- .../AGUI/Step04_HumanInLoop/Server/Program.cs | 10 ++---- .../Step05_StateManagement/Server/Program.cs | 10 ++---- .../Program.cs | 11 +++---- .../Program.cs | 13 +++----- .../Program.cs | 11 +++---- .../DeclarativeAgents/ChatClient/Program.cs | 11 +++---- .../DevUI/DevUI_Step01_BasicUsage/Program.cs | 11 +++---- .../Program.cs | 11 +++---- .../Agent_MCP_Server/Program.cs | 11 +++---- .../Agent_MCP_Server_Auth/Program.cs | 11 +++---- .../ResponseAgent_Hosted_MCP/Program.cs | 13 +++----- .../Hosted-Workflow-Handoff/Program.cs | 10 ++---- .../AGUIDojoServer/ChatClientAgentFactory.cs | 11 +++---- .../AGUIClientServer/AGUIServer/Program.cs | 11 +++---- .../05-end-to-end/AGUIWebChat/README.md | 8 ++--- .../AGUIWebChat/Server/Program.cs | 11 +++---- .../05-end-to-end/AgentWithPurview/Program.cs | 11 +++---- .../05-end-to-end/M365Agent/Program.cs | 10 ++---- dotnet/samples/Directory.Build.props | 1 + .../src/Shared/Demos/AzureOpenAIEndpoint.cs | 31 +++++++++++++++++++ 23 files changed, 110 insertions(+), 147 deletions(-) create mode 100644 dotnet/src/Shared/Demos/AzureOpenAIEndpoint.cs diff --git a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs index 0f342ccef58..ca6f46579de 100644 --- a/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step01_GettingStarted/Server/Program.cs @@ -17,13 +17,9 @@ WebApplication app = builder.Build(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] +Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -33,7 +29,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. ChatClient chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName); AIAgent agent = chatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs index e671f344344..62696e0d90f 100644 --- a/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step02_BackendTools/Server/Program.cs @@ -23,13 +23,9 @@ WebApplication app = builder.Build(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] +Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -90,7 +86,7 @@ static RestaurantSearchResponse SearchRestaurants( // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. ChatClient chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName); ChatClientAgent agent = chatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs index 0f342ccef58..ca6f46579de 100644 --- a/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step03_FrontendTools/Server/Program.cs @@ -17,13 +17,9 @@ WebApplication app = builder.Build(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] +Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -33,7 +29,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. ChatClient chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName); AIAgent agent = chatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs index 17bbcd012df..090f7466ea9 100644 --- a/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop/Server/Program.cs @@ -19,13 +19,9 @@ WebApplication app = builder.Build(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] +Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -49,7 +45,7 @@ static string ApproveExpenseReport(string expenseReportId) // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. ChatClient openAIChatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName); ChatClientAgent baseAgent = openAIChatClient.AsAIAgent( diff --git a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs index 9481d6c2bfc..6f92124bb97 100644 --- a/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs +++ b/dotnet/samples/02-agents/AGUI/Step05_StateManagement/Server/Program.cs @@ -26,13 +26,9 @@ WebApplication app = builder.Build(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] +Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); @@ -67,7 +63,7 @@ full list of ingredients (each with an icon, name and amount) and the step-by-st // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. ChatClient chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName); AIAgent baseAgent = chatClient.AsAIAgent(new ChatClientAgentOptions diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs index a1de47e8e79..1ba67f7174a 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIChatCompletion/Program.cs @@ -8,12 +8,9 @@ using OpenAI; using OpenAI.Chat; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. @@ -21,7 +18,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. AIAgent agent = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker"); diff --git a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs index 33235c349d7..be5202a1702 100644 --- a/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs +++ b/dotnet/samples/02-agents/AgentProviders/azure/Agent_With_AzureOpenAIResponses/Program.cs @@ -9,12 +9,9 @@ using OpenAI; using OpenAI.Responses; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. @@ -23,7 +20,7 @@ // You must dissable client side conversation storage for clients that support it AIAgent agent = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetResponsesClient() .AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "Joker"); @@ -35,7 +32,7 @@ // instead of being stored in the service (default). AIAgent agentStoreFalse = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetResponsesClient() .AsIChatClientWithStoredOutputDisabled(model: deploymentName) .AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker"); diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs index a25c2f5b798..d339a204c20 100644 --- a/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs +++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Program.cs @@ -17,12 +17,9 @@ using OpenAI.Responses; // --- Configuration --- -string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // --- Skills Provider --- @@ -36,7 +33,7 @@ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. -AIAgent agent = new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) +AIAgent agent = new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = endpoint }) .GetResponsesClient() .AsAIAgent(new ChatClientAgentOptions { diff --git a/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs b/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs index dd9d60030db..957d7634f39 100644 --- a/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs +++ b/dotnet/samples/02-agents/DeclarativeAgents/ChatClient/Program.cs @@ -9,12 +9,9 @@ using Microsoft.Extensions.AI; using OpenAI; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // Create the chat client @@ -23,7 +20,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. IChatClient chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsIChatClient(); diff --git a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs index cc6c5bd804f..f7428fa4f3e 100644 --- a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs +++ b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs @@ -44,18 +44,15 @@ private static void Main(string[] args) var builder = WebApplication.CreateBuilder(args); // Set up the Azure OpenAI client - var endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); - // OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. - if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) - { - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; - } + Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? "gpt-5.4-mini"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. - var chatClient = new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + var chatClient = new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsIChatClient(); diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs index b234ca52fcf..64987e00756 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Program.cs @@ -42,12 +42,9 @@ return; } -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // Launch this same assembly as a stdio MCP server in a child process. @@ -68,7 +65,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. AIAgent agent = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsAIAgent( instructions: "You answer data-analysis questions by invoking the available tools. Always invoke a tool when one matches the request.", diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs index fd1c8cddb1a..caf3327d644 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Program.cs @@ -10,12 +10,9 @@ using OpenAI; using OpenAI.Chat; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // Create an MCPClient for the GitHub server @@ -34,7 +31,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. AIAgent agent = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsAIAgent(instructions: "You answer questions related to GitHub repositories only.", tools: [.. mcpTools.Cast()]); diff --git a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs index ac5f18fee60..39bced5ea77 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Program.cs @@ -15,12 +15,9 @@ using OpenAI; using OpenAI.Chat; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // We can customize a shared HttpClient with a custom handler if desired @@ -61,7 +58,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. AIAgent agent = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsAIAgent(instructions: "You answer questions related to the weather.", tools: [.. mcpTools]); diff --git a/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs b/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs index 96ff647e9ed..d816e87b1ee 100644 --- a/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs +++ b/dotnet/samples/02-agents/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs @@ -11,12 +11,9 @@ using OpenAI; using OpenAI.Responses; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; // **** MCP Tool with Auto Approval **** @@ -38,7 +35,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. AIAgent agent = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetResponsesClient() .AsAIAgent( model: deploymentName, @@ -66,7 +63,7 @@ // Create an agent based on Azure OpenAI Responses as the backend. AIAgent agentWithRequiredApproval = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetResponsesClient() .AsAIAgent( model: deploymentName, diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs index 1320c843324..560def608d4 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/Program.cs @@ -35,13 +35,9 @@ // --------------------------------------------------------------------------- // 1. Create the shared Azure OpenAI chat client // --------------------------------------------------------------------------- -var endpointValue = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpointValue).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpointValue = $"{endpointValue.TrimEnd('/')}/openai/v1"; -} -var endpoint = new Uri(endpointValue); +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o"; // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs index 3f5e4492822..9ac945da40e 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs @@ -22,12 +22,9 @@ internal static class ChatClientAgentFactory public static void Initialize(IConfiguration configuration) { - string endpoint = configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); - // OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. - if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) - { - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; - } + Uri endpoint = AzureOpenAIEndpoint.From( + configuration["AZURE_OPENAI_ENDPOINT"]) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); s_deploymentName = configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. @@ -35,7 +32,7 @@ public static void Initialize(IConfiguration configuration) // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. s_azureOpenAIClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }); + new OpenAIClientOptions { Endpoint = endpoint }); } public static ChatClientAgent CreateAgenticChat() diff --git a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs index 53514b6ef38..b5c1f946a3c 100644 --- a/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIClientServer/AGUIServer/Program.cs @@ -15,12 +15,9 @@ builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(AGUIServerSerializerContext.Default)); builder.Services.AddAGUIServer(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); const string AgentName = "AGUIAssistant"; @@ -31,7 +28,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. IChatClient chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetResponsesClient() .AsIChatClientWithStoredOutputDisabled(model: deploymentName); diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/README.md b/dotnet/samples/05-end-to-end/AGUIWebChat/README.md index 17f34dbebd3..ffb8684d4dd 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/README.md +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/README.md @@ -67,14 +67,12 @@ The server (`Server/Program.cs`) creates a simple chat agent: ```csharp // Create OpenAI client targeting Azure OpenAI -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri openAIEndpoint = AzureOpenAIEndpoint.From(endpoint) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); OpenAIClient openAIClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }); + new OpenAIClientOptions { Endpoint = openAIEndpoint }); ChatClient chatClient = openAIClient.GetChatClient(deploymentName); diff --git a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs index da8486064ee..7cf0dfcf2f9 100644 --- a/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs +++ b/dotnet/samples/05-end-to-end/AGUIWebChat/Server/Program.cs @@ -20,12 +20,9 @@ WebApplication app = builder.Build(); -string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + builder.Configuration["AZURE_OPENAI_ENDPOINT"]) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); // Create the AI agent @@ -34,7 +31,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. OpenAIClient azureOpenAIClient = new( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }); + new OpenAIClientOptions { Endpoint = endpoint }); ChatClient chatClient = azureOpenAIClient.GetChatClient(deploymentName); diff --git a/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs b/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs index e05f02c9b2b..3cdfedacdcd 100644 --- a/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs +++ b/dotnet/samples/05-end-to-end/AgentWithPurview/Program.cs @@ -13,12 +13,9 @@ using Microsoft.Extensions.AI; using OpenAI; -var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); -// OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. -if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) -{ - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; -} +Uri endpoint = AzureOpenAIEndpoint.From( + Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")) + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; var purviewClientAppId = Environment.GetEnvironmentVariable("PURVIEW_CLIENT_APP_ID") ?? throw new InvalidOperationException("PURVIEW_CLIENT_APP_ID is not set."); @@ -35,7 +32,7 @@ // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. using IChatClient client = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetResponsesClient() .AsIChatClient(deploymentName) .AsBuilder() diff --git a/dotnet/samples/05-end-to-end/M365Agent/Program.cs b/dotnet/samples/05-end-to-end/M365Agent/Program.cs index ad33dabb02c..4bfcc7286e6 100644 --- a/dotnet/samples/05-end-to-end/M365Agent/Program.cs +++ b/dotnet/samples/05-end-to-end/M365Agent/Program.cs @@ -34,19 +34,15 @@ if (builder.Configuration.GetSection("AIServices").GetValue("UseAzureOpenAI")) { var deploymentName = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("DeploymentName")!; - var endpoint = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("Endpoint")!; - // OpenAI SDK needs the Azure OpenAI v1 route. Accept a resource root or a full /openai/v1 endpoint. - if (!new Uri(endpoint).AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) - { - endpoint = $"{endpoint.TrimEnd('/')}/openai/v1"; - } + Uri endpoint = AzureOpenAIEndpoint.From(builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("Endpoint")) + ?? throw new InvalidOperationException("AIServices:AzureOpenAI:Endpoint is not set."); // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. chatClient = new OpenAIClient( new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), - new OpenAIClientOptions { Endpoint = new Uri(endpoint) }) + new OpenAIClientOptions { Endpoint = endpoint }) .GetChatClient(deploymentName) .AsIChatClient(); } diff --git a/dotnet/samples/Directory.Build.props b/dotnet/samples/Directory.Build.props index 57767cdd5a3..cb8b5c4e0b4 100644 --- a/dotnet/samples/Directory.Build.props +++ b/dotnet/samples/Directory.Build.props @@ -11,6 +11,7 @@ + diff --git a/dotnet/src/Shared/Demos/AzureOpenAIEndpoint.cs b/dotnet/src/Shared/Demos/AzureOpenAIEndpoint.cs new file mode 100644 index 00000000000..7f7b912bdb1 --- /dev/null +++ b/dotnet/src/Shared/Demos/AzureOpenAIEndpoint.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0005 // This file is shared by projects with and without implicit usings. + +using System; + +namespace SampleHelpers; + +internal static class AzureOpenAIEndpoint +{ + public static Uri? From(string? endpoint) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + return null; + } + + Uri endpointUri = new(endpoint, UriKind.Absolute); + if (endpointUri.AbsolutePath.TrimEnd('/').EndsWith("/openai/v1", StringComparison.OrdinalIgnoreCase)) + { + return endpointUri; + } + + var endpointBuilder = new UriBuilder(endpointUri) + { + Path = $"{endpointUri.AbsolutePath.TrimEnd('/')}/openai/v1/", + }; + + return endpointBuilder.Uri; + } +}