File: OpenAIModelHealthCheck.cs
Web Access
Project: src\src\Aspire.Hosting.OpenAI\Aspire.Hosting.OpenAI.csproj (Aspire.Hosting.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 System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Diagnostics.HealthChecks;
 
namespace Aspire.Hosting.OpenAI;
 
/// <summary>
/// A health check for OpenAI Model resources.
/// </summary>
/// <param name="httpClient">The HttpClient to use.</param>
/// <param name="connectionString">The connection string.</param>
internal sealed class OpenAIModelHealthCheck(HttpClient httpClient, Func<ValueTask<string?>> connectionString) : IHealthCheck
{
    private HealthCheckResult? _result;
 
    /// <summary>
    /// Checks the health of the OpenAI endpoint by sending a test request.
    /// </summary>
    /// <param name="context">The health check context.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task that represents the asynchronous health check operation.</returns>
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        if (_result is not null)
        {
            return _result.Value;
        }
 
        try
        {
            var builder = new DbConnectionStringBuilder() { ConnectionString = await connectionString().ConfigureAwait(false) };
            var endpoint = builder["Endpoint"];
            var model = builder["Model"];
 
            using var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"{endpoint}/models/{model}"));
 
            // Add required headers
            request.Headers.Add("Authorization", $"Bearer {builder["Key"]}");
 
            using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
 
            _result = response.StatusCode switch
            {
                HttpStatusCode.OK => HealthCheckResult.Healthy(),
                HttpStatusCode.Unauthorized => HealthCheckResult.Unhealthy("OpenAI API key is invalid"),
                HttpStatusCode.NotFound => await HandleNotFound(response, cancellationToken).ConfigureAwait(false),
                HttpStatusCode.TooManyRequests => HealthCheckResult.Unhealthy("OpenAI API rate limit exceeded"),
                _ => HealthCheckResult.Unhealthy($"OpenAI endpoint returned unexpected status code: {response.StatusCode}")
            };
        }
        catch (Exception ex)
        {
            _result = HealthCheckResult.Unhealthy($"Failed to check OpenAI endpoint: {ex.Message}", ex);
        }
 
        return _result.Value;
    }
 
    private static async Task<HealthCheckResult> HandleNotFound(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        OpenAIErrorResponse? errorResponse = null;
 
        try
        {
            var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
            errorResponse = JsonSerializer.Deserialize<OpenAIErrorResponse>(content);
 
            if (errorResponse?.Error?.Code == "model_not_found")
            {
                var message = !string.IsNullOrEmpty(errorResponse.Error.Message)
                    ? errorResponse.Error.Message
                    : "Model not found";
                return HealthCheckResult.Unhealthy($"OpenAI: {message}");
            }
        }
        catch
        {
        }
 
        return HealthCheckResult.Unhealthy($"OpenAI returned an unsupported response: ({response.StatusCode}) {errorResponse?.Error?.Message}");
    }
 
    /// <summary>
    /// Represents the error response from OpenAI API.
    /// </summary>
    private sealed class OpenAIErrorResponse
    {
        [JsonPropertyName("error")]
        public OpenAIError? Error { get; set; }
    }
 
    /// <summary>
    /// Represents an error from OpenAI API.
    /// </summary>
    private sealed class OpenAIError
    {
        [JsonPropertyName("code")]
        public string? Code { get; set; }
 
        [JsonPropertyName("message")]
        public string? Message { get; set; }
 
        [JsonPropertyName("type")]
        public string? Type { get; set; }
    }
}