From d727bf33b9e66c68fbd78c0b1f5792fd146e7400 Mon Sep 17 00:00:00 2001 From: SparkyPunk Date: Fri, 16 Oct 2026 13:09:26 +0530 Subject: [PATCH 1/2] dotnet: add Azure AI Foundry api-key header handler sample (#10455) Signed-off-by: sparkysolutions10-pixelmy --- .../Connectors_CustomHttpClient.cs | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs b/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs index 54de56688cdd..f619aed4dffb 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs @@ -1,6 +1,12 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; namespace ChatCompletion; @@ -36,4 +42,46 @@ public void UseCustomHttpClient() httpClient: httpClient) .Build(); } + + /// + /// Demonstrates how to use a custom DelegatingHandler to send the 'api-key' header + /// required by Azure AI Foundry / Model Catalog deployments (resolves #10455). + /// + [Fact] + public void UseCustomHttpClientWithApiKeyHeaderForAzureAIFoundry() + { + // Azure AI Foundry model inference endpoints (e.g. Phi-3, Mistral, Llama) require an 'api-key' header + // rather than the standard OpenAI 'Authorization: Bearer' token. + using var handler = new AzureAIFoundryApiKeyHandler("test-api-key"); + using var httpClient = new HttpClient(handler); + + var kernel = Kernel.CreateBuilder() + .AddOpenAIChatCompletion( + modelId: "Phi-3-small-8k-instruct", + endpoint: new Uri("https://my-resource.services.ai.azure.com/models"), + apiKey: "test-api-key", + httpClient: httpClient) + .Build(); + + Assert.NotNull(kernel); + } + + /// + /// DelegatingHandler that injects the 'api-key' header for Azure AI Foundry and OpenAI-compatible endpoints. + /// + private sealed class AzureAIFoundryApiKeyHandler(string apiKey) : DelegatingHandler(new HttpClientHandler()) + { + private readonly string _apiKey = apiKey; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // Ensure the required 'api-key' header is present on outgoing requests + if (!request.Headers.Contains("api-key")) + { + request.Headers.Add("api-key", this._apiKey); + } + + return base.SendAsync(request, cancellationToken); + } + } } From f4ec05fd48c5eb79331c6bf34095bdeb6fe67e36 Mon Sep 17 00:00:00 2001 From: SparkyPunk Date: Fri, 16 Oct 2026 13:26:21 +0530 Subject: [PATCH 2/2] dotnet: add api-version query injection and isolated recording tests (#10455) Addresses Copilot code-review feedback by appending api-version and adding in-memory unit tests. Signed-off-by: sparkysolutions10-pixelmy --- .../Connectors_CustomHttpClient.cs | 94 +++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs b/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs index f619aed4dffb..30df3f43870a 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Connectors_CustomHttpClient.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -45,13 +47,13 @@ public void UseCustomHttpClient() /// /// Demonstrates how to use a custom DelegatingHandler to send the 'api-key' header - /// required by Azure AI Foundry / Model Catalog deployments (resolves #10455). + /// and 'api-version' parameter required by Azure AI Foundry / Model Catalog deployments (resolves #10455). /// [Fact] public void UseCustomHttpClientWithApiKeyHeaderForAzureAIFoundry() { // Azure AI Foundry model inference endpoints (e.g. Phi-3, Mistral, Llama) require an 'api-key' header - // rather than the standard OpenAI 'Authorization: Bearer' token. + // and an 'api-version' query parameter rather than the standard OpenAI 'Authorization: Bearer' token. using var handler = new AzureAIFoundryApiKeyHandler("test-api-key"); using var httpClient = new HttpClient(handler); @@ -67,21 +69,101 @@ public void UseCustomHttpClientWithApiKeyHeaderForAzureAIFoundry() } /// - /// DelegatingHandler that injects the 'api-key' header for Azure AI Foundry and OpenAI-compatible endpoints. + /// Verifies that AzureAIFoundryApiKeyHandler injects the 'api-key' header and 'api-version' + /// query parameter, and preserves existing values without contacting external services. /// - private sealed class AzureAIFoundryApiKeyHandler(string apiKey) : DelegatingHandler(new HttpClientHandler()) + [Fact] + public async Task AzureAIFoundryApiKeyHandler_InjectsHeaderAndApiVersionAsync() + { + HttpRequestMessage? capturedRequest = null; + var innerHandler = new RecordingHandler(req => capturedRequest = req); + using var handler = new AzureAIFoundryApiKeyHandler("my-secret-key", "2024-05-01-preview", innerHandler); + using var invoker = new HttpMessageInvoker(handler); + + using var request = new HttpRequestMessage(HttpMethod.Post, "https://my-resource.services.ai.azure.com/models/chat/completions"); + using var response = await invoker.SendAsync(request, CancellationToken.None); + + Assert.NotNull(capturedRequest); + Assert.True(capturedRequest.Headers.Contains("api-key")); + Assert.Equal("my-secret-key", capturedRequest.Headers.GetValues("api-key").First()); + Assert.NotNull(capturedRequest.RequestUri); + Assert.Contains("api-version=2024-05-01-preview", capturedRequest.RequestUri.Query); + } + + /// + /// Verifies that an existing 'api-key' header is preserved if already set on the request. + /// + [Fact] + public async Task AzureAIFoundryApiKeyHandler_PreservesExistingHeaderAsync() + { + HttpRequestMessage? capturedRequest = null; + var innerHandler = new RecordingHandler(req => capturedRequest = req); + using var handler = new AzureAIFoundryApiKeyHandler("default-key", "2024-05-01-preview", innerHandler); + using var invoker = new HttpMessageInvoker(handler); + + using var request = new HttpRequestMessage(HttpMethod.Post, "https://my-resource.services.ai.azure.com/models/chat/completions?existing=1"); + request.Headers.Add("api-key", "explicit-key"); + using var response = await invoker.SendAsync(request, CancellationToken.None); + + Assert.NotNull(capturedRequest); + Assert.Equal("explicit-key", capturedRequest.Headers.GetValues("api-key").First()); + Assert.NotNull(capturedRequest.RequestUri); + Assert.Contains("existing=1", capturedRequest.RequestUri.Query); + Assert.Contains("api-version=2024-05-01-preview", capturedRequest.RequestUri.Query); + } + + /// + /// DelegatingHandler that injects the 'api-key' header and 'api-version' query parameter + /// for Azure AI Foundry and OpenAI-compatible endpoints. + /// + private sealed class AzureAIFoundryApiKeyHandler : DelegatingHandler { - private readonly string _apiKey = apiKey; + private readonly string _apiKey; + private readonly string _apiVersion; + + public AzureAIFoundryApiKeyHandler(string apiKey, string apiVersion = "2024-05-01-preview", HttpMessageHandler? innerHandler = null) + : base(innerHandler ?? new HttpClientHandler()) + { + this._apiKey = apiKey; + this._apiVersion = apiVersion; + } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - // Ensure the required 'api-key' header is present on outgoing requests + // 1. Ensure required 'api-key' header is present if (!request.Headers.Contains("api-key")) { request.Headers.Add("api-key", this._apiKey); } + // 2. Ensure required 'api-version' query parameter is present on inference endpoint + if (request.RequestUri is not null) + { + var uriBuilder = new UriBuilder(request.RequestUri); + if (!uriBuilder.Query.Contains("api-version=")) + { + var query = string.IsNullOrEmpty(uriBuilder.Query) ? string.Empty : uriBuilder.Query.TrimStart('?') + "&"; + query += $"api-version={Uri.EscapeDataString(this._apiVersion)}"; + uriBuilder.Query = query; + request.RequestUri = uriBuilder.Uri; + } + } + return base.SendAsync(request, cancellationToken); } } + + /// + /// In-memory handler for testing HTTP request transformations without live network calls. + /// + private sealed class RecordingHandler(Action onRequest) : HttpMessageHandler + { + private readonly Action _onRequest = onRequest; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this._onRequest(request); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + } }