File: AspireConfigurableOpenAIExtensions.cs
Web Access
Project: src\src\Components\Aspire.Azure.AI.Extensions.OpenAI\Aspire.Azure.AI.Extensions.OpenAI.csproj (Aspire.Azure.AI.Extensions.OpenAI)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Data.Common;
using Aspire.OpenAI;
using Microsoft.Extensions.Configuration;
using OpenAI;
 
namespace Microsoft.Extensions.Hosting;
 
/// <summary>
/// Registers OpenAI clients with endpoint and authentication selection based on connection configuration.
/// </summary>
public static class AspireConfigurableOpenAIExtensions
{
    /// <summary>
    /// Registers an <see cref="OpenAIClient"/> singleton, selecting OpenAI-compatible, Azure account,
    /// or Foundry project registration from the connection string.
    /// </summary>
    /// <param name="builder">The application builder.</param>
    /// <param name="connectionName">The connection-string name.</param>
    /// <returns>A builder for registering chat and embedding services.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="builder"/> is null.</exception>
    /// <exception cref="ArgumentException">The name is empty or connection information is malformed.</exception>
    /// <exception cref="InvalidOperationException">The connection string has neither an endpoint nor an API key.</exception>
    /// <remarks>
    /// Azure endpoints use Microsoft Entra authentication unless an account API key is supplied.
    /// Other endpoints, including Foundry Local, use the configured API key.
    /// The <c>IsAzure</c> connection-string field overrides Azure account detection for custom domains.
    /// </remarks>
    /// <example>
    /// <code>
    /// builder.AddOpenAIClientFromConfiguration("chat").AddChatClient();
    /// </code>
    /// </example>
    public static AspireOpenAIClientBuilder AddOpenAIClientFromConfiguration(
        this IHostApplicationBuilder builder, string connectionName)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(connectionName);
 
        return GetClientKind(builder.Configuration, connectionName) switch
        {
            ClientKind.AzureAccount => builder.AddAzureOpenAIClient(connectionName),
            ClientKind.FoundryProject => builder.AddAzureProjectOpenAIClient(connectionName),
            _ => builder.AddOpenAIClient(connectionName)
        };
    }
 
    /// <summary>
    /// Registers a keyed <see cref="OpenAIClient"/> singleton, selecting OpenAI-compatible, Azure account,
    /// or Foundry project registration from the connection string.
    /// </summary>
    /// <param name="builder">The application builder.</param>
    /// <param name="name">The service key and connection-string name.</param>
    /// <returns>A builder for registering keyed chat and embedding services.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="builder"/> is null.</exception>
    /// <exception cref="ArgumentException">The name is empty or connection information is malformed.</exception>
    /// <exception cref="InvalidOperationException">The connection string has neither an endpoint nor an API key.</exception>
    /// <remarks>Uses the same endpoint and authentication selection as <see cref="AddOpenAIClientFromConfiguration"/>.</remarks>
    /// <example>
    /// <code>
    /// builder.AddKeyedOpenAIClientFromConfiguration("chat").AddKeyedChatClient("chat-client");
    /// </code>
    /// </example>
    public static AspireOpenAIClientBuilder AddKeyedOpenAIClientFromConfiguration(
        this IHostApplicationBuilder builder, string name)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        return GetClientKind(builder.Configuration, name) switch
        {
            ClientKind.AzureAccount => builder.AddKeyedAzureOpenAIClient(name),
            ClientKind.FoundryProject => builder.AddKeyedAzureProjectOpenAIClient(name),
            _ => builder.AddKeyedOpenAIClient(name)
        };
    }
 
    private static ClientKind GetClientKind(IConfiguration configuration, string connectionName)
    {
        if (!configuration.TryGetConnectionString(connectionName, out var connectionString))
        {
            // Preserve the original helper's default: settings-only registrations use Aspire:OpenAI.
            return ClientKind.OpenAI;
        }
 
        Uri? endpoint;
        bool? isAzure = null;
        string? key = null;
        var bareEndpoint = Uri.TryCreate(connectionString, UriKind.Absolute, out var uri);
        if (bareEndpoint)
        {
            endpoint = uri;
        }
        else
        {
            // Hosting supplies "Endpoint=...;Key=...;Model=..." locally and
            // "Endpoint=...;EndpointAIInference=...;Deployment=..." for Azure Foundry.
            var values = new DbConnectionStringBuilder { ConnectionString = connectionString };
            endpoint = null;
            if (values.TryGetValue("Endpoint", out var rawEndpoint))
            {
                if (!Uri.TryCreate(rawEndpoint.ToString(), UriKind.Absolute, out endpoint))
                {
                    throw new ArgumentException($"The 'Endpoint' value in 'ConnectionStrings:{connectionName}' must be an absolute HTTP or HTTPS URI.", nameof(connectionName));
                }
            }
 
            key = values.TryGetValue("Key", out var rawKey) ? rawKey.ToString()?.Trim() : null;
            if (values.TryGetValue("IsAzure", out var rawIsAzure))
            {
                if (!bool.TryParse(rawIsAzure.ToString(), out var parsedIsAzure))
                {
                    throw new ArgumentException($"The 'IsAzure' value in 'ConnectionStrings:{connectionName}' must be 'true' or 'false'.", nameof(connectionName));
                }
 
                isAzure = parsedIsAzure;
            }
        }
 
        if (endpoint is not null && endpoint.Scheme is not ("http" or "https"))
        {
            throw new ArgumentException($"The 'Endpoint' value in 'ConnectionStrings:{connectionName}' must be an absolute HTTP or HTTPS URI.", nameof(connectionName));
        }
 
        if (endpoint is null && string.IsNullOrEmpty(key))
        {
            throw new InvalidOperationException($"An OpenAIClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}'.");
        }
 
        if (isAzure == false)
        {
            return ClientKind.OpenAI;
        }
 
        var azure = isAzure == true || (endpoint is not null && IsAzureHost(endpoint.Host));
        if (!azure)
        {
            if (bareEndpoint)
            {
                throw new ArgumentException($"OpenAI-compatible endpoints require 'Endpoint=...;Key=...' in 'ConnectionStrings:{connectionName}'.", nameof(connectionName));
            }
 
            return ClientKind.OpenAI;
        }
 
        return endpoint is not null && endpoint.AbsolutePath.StartsWith("/api/projects/", StringComparison.OrdinalIgnoreCase)
            ? ClientKind.FoundryProject : ClientKind.AzureAccount;
    }
 
    private static bool IsAzureHost(string host)
        => host.EndsWith(".azure.com", StringComparison.OrdinalIgnoreCase) ||
           host.EndsWith(".azure.us", StringComparison.OrdinalIgnoreCase) ||
           host.EndsWith(".azure.cn", StringComparison.OrdinalIgnoreCase) ||
           host.EndsWith(".azure.de", StringComparison.OrdinalIgnoreCase);
 
    private enum ClientKind
    {
        OpenAI,
        AzureAccount,
        FoundryProject
    }
}