diff --git a/src/Aspire.Hosting.OpenAI/OpenAIExtensions.cs b/src/Aspire.Hosting.OpenAI/OpenAIExtensions.cs index 202ac7f73ca..776690dd619 100644 --- a/src/Aspire.Hosting.OpenAI/OpenAIExtensions.cs +++ b/src/Aspire.Hosting.OpenAI/OpenAIExtensions.cs @@ -42,13 +42,18 @@ You can obtain an API key from the [OpenAI API Keys page](https://platform.opena // Register the health check var healthCheckKey = $"{name}_check"; - builder.AddStatusPageCheck( - healthCheckKey, - statusJsonUrl: "https://status.openai.com/api/v2/status.json", - httpClientName: "OpenAIHealthCheck", - timeout: TimeSpan.FromSeconds(5), + // Ensure IHttpClientFactory is available by registering HTTP client services + builder.Services.AddHttpClient(); + + builder.Services.AddHealthChecks().Add(new HealthCheckRegistration( + name: healthCheckKey, + factory: sp => + { + var httpFactory = sp.GetRequiredService(); + return new OpenAIHealthCheck(httpFactory, resource, "OpenAIHealthCheck", TimeSpan.FromSeconds(5)); + }, failureStatus: HealthStatus.Unhealthy, - tags: ["openai", "healthcheck"]); + tags: ["openai", "healthcheck"])); return builder.AddResource(resource) .WithInitialState(new() diff --git a/src/Aspire.Hosting.OpenAI/StatusPageHealthCheck.cs b/src/Aspire.Hosting.OpenAI/OpenAIHealthCheck.cs similarity index 69% rename from src/Aspire.Hosting.OpenAI/StatusPageHealthCheck.cs rename to src/Aspire.Hosting.OpenAI/OpenAIHealthCheck.cs index 6756c83a02f..ec4e8e5a06f 100644 --- a/src/Aspire.Hosting.OpenAI/StatusPageHealthCheck.cs +++ b/src/Aspire.Hosting.OpenAI/OpenAIHealthCheck.cs @@ -3,40 +3,58 @@ using System.Text.Json; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting.OpenAI; /// -/// Checks a StatusPage "status.json" endpoint and maps indicator to ASP.NET Core health status. +/// Health check for OpenAI resources that adapts based on endpoint configuration. /// -internal sealed class StatusPageHealthCheck : IHealthCheck +internal sealed class OpenAIHealthCheck : IHealthCheck { + private static readonly Uri s_defaultEndpointUri = new("https://api.openai.com/v1"); + private static readonly Uri s_statusPageUri = new("https://status.openai.com/api/v2/status.json"); + private readonly IHttpClientFactory _httpClientFactory; - private readonly Uri _statusEndpoint; + private readonly OpenAIResource _resource; private readonly string? _httpClientName; private readonly TimeSpan _timeout; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The factory to create HTTP clients. - /// The URI of the status.json endpoint. + /// The OpenAI resource. /// The optional name of the HTTP client to use. /// The optional timeout for the HTTP request. - public StatusPageHealthCheck( + public OpenAIHealthCheck( IHttpClientFactory httpClientFactory, - Uri statusEndpoint, + OpenAIResource resource, string? httpClientName = null, TimeSpan? timeout = null) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - _statusEndpoint = statusEndpoint ?? throw new ArgumentNullException(nameof(statusEndpoint)); + _resource = resource ?? throw new ArgumentNullException(nameof(resource)); _httpClientName = httpClientName; _timeout = timeout ?? TimeSpan.FromSeconds(5); } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + // Case 1: Default endpoint - check StatusPage + if (Uri.TryCreate(_resource.Endpoint, UriKind.Absolute, out var endpointUri) && + Uri.Compare(endpointUri, s_defaultEndpointUri, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0) + { + return await CheckStatusPageAsync(cancellationToken).ConfigureAwait(false); + } + + // Case 2: Custom endpoint - return healthy + return HealthCheckResult.Healthy("Custom OpenAI endpoint configured"); + } + + /// + /// Checks the StatusPage endpoint for the default OpenAI service. + /// + private async Task CheckStatusPageAsync(CancellationToken cancellationToken) { var client = string.IsNullOrWhiteSpace(_httpClientName) ? _httpClientFactory.CreateClient() @@ -45,7 +63,7 @@ public async Task CheckHealthAsync(HealthCheckContext context using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(_timeout); - using var req = new HttpRequestMessage(HttpMethod.Get, _statusEndpoint); + using var req = new HttpRequestMessage(HttpMethod.Get, s_statusPageUri); req.Headers.Accept.ParseAdd("application/json"); HttpResponseMessage resp; @@ -94,7 +112,7 @@ public async Task CheckHealthAsync(HealthCheckContext context { ["indicator"] = indicator, ["description"] = description, - ["endpoint"] = _statusEndpoint.ToString() + ["endpoint"] = s_statusPageUri.ToString() }; // Map indicator -> HealthStatus @@ -113,38 +131,3 @@ public async Task CheckHealthAsync(HealthCheckContext context } } } - -internal static class StatuspageHealthCheckExtensions -{ - /// - /// Registers a StatusPage health check for a given status.json URL. - /// - public static IDistributedApplicationBuilder AddStatusPageCheck( - this IDistributedApplicationBuilder builder, - string name, - string statusJsonUrl, - string? httpClientName = null, - TimeSpan? timeout = null, - HealthStatus? failureStatus = null, - IEnumerable? tags = null) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentException.ThrowIfNullOrWhiteSpace(name); - ArgumentException.ThrowIfNullOrWhiteSpace(statusJsonUrl); - - // Ensure IHttpClientFactory is available by registering HTTP client services - builder.Services.AddHttpClient(); - - builder.Services.AddHealthChecks().Add(new HealthCheckRegistration( - name: name, - factory: sp => - { - var httpFactory = sp.GetRequiredService(); - return new StatusPageHealthCheck(httpFactory, new Uri(statusJsonUrl), httpClientName, timeout); - }, - failureStatus: failureStatus, - tags: tags)); - - return builder; - } -}