⚠️ This blog post was created with the help of AI tools. Yes, I used a bit of magic from language models to organize my thoughts and automate the boring parts, but the geeky fun and the 🤖 in C# are 100% mine.

Hi!

This post demonstrates how to use Anthropic’s Claude models deployed in Microsoft Foundry with the Microsoft.Extensions.AI (MEAI) framework. Since official SDK support for Claude models in MEAI is not yet available, this sample shows a practical approach to bridge the gap between OpenAI’s API format (used by MEAI) and Claude’s native Anthropic API format.

7 min video overview, so you can jump directly to the code:

Claude in Microsoft Foundry, so cool!

 Example of a Claude model (claude-haiku-4-5) deployed in Azure Foundry

Important Note: This is demo/educational code intended to show the integration pattern until official library support for Claude models becomes available. For production scenarios, wait for official SDK support.

The Problem

Microsoft.Extensions.AI provides a unified IChatClient interface that works seamlessly with Azure OpenAI models. However, Claude models in Microsoft Foundry use Anthropic’s native API format, which differs from OpenAI’s format in several key ways:

AspectOpenAI FormatClaude Format
AuthenticationAuthorization: Bearer {key}x-api-key: {key}
API Version HeaderNone requiredanthropic-version: 2023-06-01
Max Tokens Fieldmax_completion_tokensmax_tokens (required)
System MessagesIn messages arraySeparate system parameter
Streaming Eventsdata: [DONE]event: message_stop

These differences mean that MEAI’s Azure OpenAI client cannot directly communicate with Claude models without transformation.

The Solution

I created a custom DelegatingHandler that intercepts HTTP requests/responses between MEAI and Claude’s API, transforming the data format in both directions. This allows us to use MEAI’s IChatClient interface while communicating with Claude models.

How it works

  1. The application creates an AzureOpenAIClient with a custom HTTP transport
  2. The ClaudeToOpenAIMessageHandler intercepts requests to Claude deployments
  3. It transforms OpenAI-style requests to Claude Messages API format:
    • Converts message format and extracts system messages
    • Skips empty messages (Claude requires non-empty content)
    • Adds required Claude headers (x-api-keyanthropic-version)
    • Transforms endpoint URL to Claude’s format
  4. Streaming responses are transformed back to OpenAI format using SSE (Server-Sent Events)
  5. All responses maintain OpenAI API compatibility

Architecture Overview

┌─────────────────────────────────────┐
│   Your Application (Program.cs)    │
│                                     │
│  IChatClient chatClient             │
└──────────────┬──────────────────────┘
               │
               │ OpenAI Format
               ▼
┌─────────────────────────────────────┐
│  ClaudeToOpenAIMessageHandler       │
│                                     │
│  ┌─────────────────────────────┐   │
│  │ Request Transformation      │   │
│  │ OpenAI → Claude Format      │   │
│  └─────────────────────────────┘   │
│                                     │
│  ┌─────────────────────────────┐   │
│  │ Response Transformation     │   │
│  │ Claude → OpenAI Format      │   │
│  └─────────────────────────────┘   │
│                                     │
│  ┌─────────────────────────────┐   │
│  │ Streaming Transformation    │   │
│  │ SSE Event Conversion        │   │
│  └─────────────────────────────┘   │
└──────────────┬──────────────────────┘
               │
               │ Claude Format
               ▼
┌─────────────────────────────────────┐
│   Microsoft Foundry                  │
│   (Claude Models API)               │
└─────────────────────────────────────┘

Step-by-Step Implementation

Step 1: Create the Custom Handler Class

The ClaudeToOpenAIMessageHandler extends DelegatingHandler to intercept HTTP traffic:

public class ClaudeToOpenAIMessageHandler : DelegatingHandler
{
    // Constants for Claude API
    private const string ClaudeAnthropicVersion = "2023-06-01";
    private const string ClaudeEventTypeContentDelta = "content_block_delta";
    private const string ClaudeEventTypeMessageStop = "message_stop";
    private const int DefaultMaxTokens = 2048;

    // Required properties
    public required string AzureClaudeDeploymentUrl { get; init; }
    public required string ApiKey { get; init; }
    public required string Model { get; init; }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        // Detect if this is a request that needs Claude transformation
        if (IsClaudeRequest(request))
        {
            // Transform and send to Claude API
            return await TransformRequestToClaude(request, cancellationToken);
        }

        // Pass through other requests unchanged
        return await base.SendAsync(request, cancellationToken);
    }
}

Step 2: Transform the Request Body

Convert OpenAI’s JSON format to Claude’s format:

private async Task<HttpRequestMessage> TransformRequestToClaude(
    HttpRequestMessage request, 
    CancellationToken cancellationToken)
{
    // Read the original OpenAI-format request
    var originalBody = await request.Content!.ReadAsStringAsync(cancellationToken);
    var openAiRequest = JsonSerializer.Deserialize<JsonElement>(originalBody);

    // Extract messages and convert to Claude format
    var messages = ConvertMessagesToClaudeFormat(openAiRequest);
    
    // Build Claude request body
    var claudeRequestBody = BuildClaudeRequestBody(openAiRequest, messages);

    // Create new request with Claude endpoint
    var claudeRequest = new HttpRequestMessage(HttpMethod.Post, AzureClaudeDeploymentUrl)
    {
        Content = new StringContent(
            JsonSerializer.Serialize(claudeRequestBody),
            Encoding.UTF8,
            "application/json")
    };

    // Add Claude-specific headers
    ConfigureClaudeRequestHeaders(claudeRequest);

    return claudeRequest;
}

Step 3: Handle Authentication Headers

Claude requires x-api-key instead of Authorization: Bearer:

private void ConfigureClaudeRequestHeaders(HttpRequestMessage claudeRequest)
{
    // Claude uses x-api-key header, not Authorization Bearer
    claudeRequest.Headers.Add("x-api-key", ApiKey);
    
    // Required API version header
    claudeRequest.Headers.Add("anthropic-version", ClaudeAnthropicVersion);
}

Step 4: Convert Message Format

Extract system messages and format the conversation:

private List<JsonElement> ConvertMessagesToClaudeFormat(JsonElement openAiRequest)
{
    var messages = new List<JsonElement>();
    
    if (openAiRequest.TryGetProperty("messages", out var messagesArray))
    {
        foreach (var message in messagesArray.EnumerateArray())
        {
            // Extract content
            var content = message.TryGetProperty("content", out var contentProp)
                ? contentProp.GetString() ?? string.Empty
                : string.Empty;

            // Skip empty messages (Claude validates this)
            if (string.IsNullOrWhiteSpace(content))
                continue;

            var role = message.TryGetProperty("role", out var roleProp)
                ? roleProp.GetString() ?? string.Empty
                : string.Empty;

            // Skip system messages (handled separately in Claude)
            if (role == "system")
                continue;

            messages.Add(message);
        }
    }
    
    return messages;
}

Step 5: Build Claude Request Body

Construct the proper JSON structure with required fields:

private Dictionary<string, object> BuildClaudeRequestBody(
    JsonElement openAiRequest, 
    List<JsonElement> messages)
{
    var claudeRequestBody = new Dictionary<string, object>
    {
        ["model"] = Model,
        ["messages"] = messages,
        // Claude requires max_tokens (not max_completion_tokens)
        ["max_tokens"] = openAiRequest.TryGetProperty("max_tokens", out var maxTokens)
            ? maxTokens.GetInt32()
            : DefaultMaxTokens
    };

    // Extract system message if present
    var systemMessage = ExtractSystemMessage(openAiRequest);
    if (!string.IsNullOrWhiteSpace(systemMessage))
    {
        claudeRequestBody["system"] = systemMessage;
    }

    // Handle streaming flag
    if (openAiRequest.TryGetProperty("stream", out var streamProp))
    {
        claudeRequestBody["stream"] = streamProp.GetBoolean();
    }

    return claudeRequestBody;
}

Step 6: Transform Streaming Responses

Convert Claude’s Server-Sent Events (SSE) to OpenAI format using System.IO.Pipelines:

private async Task<HttpResponseMessage> TransformStreamingResponse(
    HttpResponseMessage claudeResponse, 
    CancellationToken cancellationToken)
{
    var pipe = new Pipe();
    var writer = pipe.Writer;
    var reader = pipe.Reader;

    // Background task to read Claude's stream and write OpenAI format
    _ = Task.Run(async () =>
    {
        try
        {
            var stream = await claudeResponse.Content.ReadAsStreamAsync(cancellationToken);
            using var streamReader = new StreamReader(stream);

            while (!streamReader.EndOfStream)
            {
                var line = await streamReader.ReadLineAsync(cancellationToken);
                if (string.IsNullOrWhiteSpace(line))
                    continue;

                // Process Claude SSE events
                if (line.StartsWith("event:"))
                {
                    var eventType = line.Substring(6).Trim();
                    
                    // Read the data line
                    var dataLine = await streamReader.ReadLineAsync(cancellationToken);
                    if (dataLine?.StartsWith("data:") == true)
                    {
                        var eventData = dataLine.Substring(5).Trim();
                        
                        // Transform event to OpenAI format
                        var openAiChunk = ProcessClaudeDataEvent(eventType, eventData);
                        if (!string.IsNullOrEmpty(openAiChunk))
                        {
                            var bytes = Encoding.UTF8.GetBytes(openAiChunk);
                            await writer.WriteAsync(bytes, cancellationToken);
                        }
                    }
                }
            }
        }
        finally
        {
            await writer.CompleteAsync();
        }
    }, cancellationToken);

    // Return response with transformed stream
    return new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StreamContent(reader.AsStream())
        {
            Headers = { ContentType = new MediaTypeHeaderValue("text/event-stream") }
        }
    };
}

Step 7: Process Claude Events

Convert individual SSE events to OpenAI format:

private string ProcessClaudeDataEvent(string eventType, string eventData)
{
    switch (eventType)
    {
        case ClaudeEventTypeContentDelta:
            // Extract text from Claude's nested structure
            var claudeEvent = JsonSerializer.Deserialize<JsonElement>(eventData);
            if (claudeEvent.TryGetProperty("delta", out var delta) &&
                delta.TryGetProperty("text", out var text))
            {
                var textContent = text.GetString();
                
                // Build OpenAI-format chunk
                var openAiChunk = new
                {
                    id = "chatcmpl-" + Guid.NewGuid().ToString(),
                    @object = "chat.completion.chunk",
                    created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    model = Model,
                    choices = new[]
                    {
                        new
                        {
                            index = 0,
                            delta = new { content = textContent },
                            finish_reason = (string?)null
                        }
                    }
                };

                return "data: " + JsonSerializer.Serialize(openAiChunk) + "\n\n";
            }
            break;

        case ClaudeEventTypeMessageStop:
            // Send completion signal
            var stopChunk = new
            {
                id = "chatcmpl-" + Guid.NewGuid().ToString(),
                @object = "chat.completion.chunk",
                created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                model = Model,
                choices = new[]
                {
                    new
                    {
                        index = 0,
                        delta = new { },
                        finish_reason = "stop"
                    }
                }
            };

            return "data: " + JsonSerializer.Serialize(stopChunk) + "\n\ndata: [DONE]\n\n";
    }

    return string.Empty;
}

Run the app

1. Set Up User Secrets

Store your Microsoft Foundry credentials securely:

# Navigate to the project directory
cd samples/CoreSamples/BasicChat-11FoundryClaude

# Set the secrets
cd samples/CoreSamples/BasicChat-11FoundryClaude
dotnet user-secrets set "endpoint" "https://<your-resource-name>.cognitiveservices.azure.com"
dotnet user-secrets set "endpointClaude" "https://<your-resource-name>.services.ai.azure.com/anthropic/v1/messages"
dotnet user-secrets set "apikey" "<your-api-key>"
dotnet user-secrets set "deploymentName" "claude-haiku-4-5"

2. Create the MEAI Client

Wire up the custom handler with MEAI’s IChatClient:

using Microsoft.Extensions.AI;
using Azure.AI.OpenAI;

// Load configuration
var config = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .Build();

var apiKey = config["AZURE_CLAUDE_APIKEY"];
var endpoint = config["AZURE_CLAUDE_ENDPOINT"];
var model = config["AZURE_CLAUDE_MODEL"];

// Create the custom handler
var handler = new ClaudeToOpenAIMessageHandler
{
    AzureClaudeDeploymentUrl = endpoint,
    ApiKey = apiKey,
    Model = model
};

// Wrap in HttpClient for Azure SDK
var httpClient = new HttpClient(handler);

// Create Azure OpenAI client with custom transport
var transport = new HttpClientPipelineTransport(httpClient);
var azureClient = new AzureOpenAIClient(new Uri(endpoint), new(), transport);

// Get IChatClient interface
IChatClient chatClient = azureClient
    .GetChatClient(model)
    .AsIChatClient();

3. Use the Chat Client

Now you can use the standard MEAI interface:

var messages = new List<ChatMessage>();

while (true)
{
    Console.Write("You: ");
    var userInput = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userInput))
        break;

    messages.Add(new ChatMessage(ChatRole.User, userInput));

    Console.Write("Assistant: ");
    
    // Stream the response
    await foreach (var chunk in chatClient.CompleteStreamingAsync(messages))
    {
        Console.Write(chunk.Text);
    }
    
    Console.WriteLine();
}

When to Use This Approach

✅ Good for:

  • Learning how API format differences work
  • Prototyping with Claude models in Microsoft Foundry
  • Understanding MEAI’s extensibility model
  • Educational purposes and experimentation

❌ Not recommended for:

  • Production applications (wait for official support)
  • Everything else!

Conclusion

This sample demonstrates a practical and temporary bridge between Microsoft.Extensions.AI and Claude models in Microsoft Foundry. While it’s functional for learning and prototyping, it’s important to remember this is demo code that fills a gap until official support becomes available.

Resources

Microsoft Documentation

Anthropic Documentation

Code Repository

💡 Tip: Check out the complete working sample to see the full implementation including ClaudeToOpenAIMessageHandler.csProgram.cs, and all configuration files.

2 responses to “Using Claude Models in Microsoft Foundry with Microsoft.Extensions.AI (temp fix 😏)”

  1. […] Using Claude Models in Microsoft Foundry with Microsoft.Extensions.AI (temp fix 😏) (Bruno Capuano) […]

    Like

  2. […] Microsoft Extensions for AI (MEAI) is a new set of .NET libraries providing unified interfaces (like IChatClient) to interact with AI models (Azure OpenAI, local models, etc.). It’s the backbone for building AI features in .NET apps (and is used in Microsoft’s AI Agent Framework). Ideally, MEAI will eventually support Anthropic’s Claude natively – however, as of now (late 2025) the official MEAI does not yet have built-in support for Claude deployments in Foundry. The issue is that Claude’s API format differs from OpenAI’s, so the current Azure OpenAI clients can’t directly talk to Claude without some tweaks (elbruno.com). […]

    Like

Leave a comment

Discover more from El Bruno

Subscribe now to keep reading and get access to the full archive.

Continue reading