Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/Aspire.Hosting.OpenAI/OpenAIExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IHttpClientFactory>();
return new OpenAIHealthCheck(httpFactory, resource, "OpenAIHealthCheck", TimeSpan.FromSeconds(5));
},
failureStatus: HealthStatus.Unhealthy,
tags: ["openai", "healthcheck"]);
tags: ["openai", "healthcheck"]));

return builder.AddResource(resource)
.WithInitialState(new()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,68 @@

using System.Text.Json;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting.OpenAI;

/// <summary>
/// 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.
/// </summary>
internal sealed class StatusPageHealthCheck : IHealthCheck
internal sealed class OpenAIHealthCheck : IHealthCheck
{
private const string DefaultEndpoint = "https://api.openai.com/v1";
private readonly IHttpClientFactory _httpClientFactory;
private readonly Uri _statusEndpoint;
private readonly OpenAIResource _resource;
private readonly string? _httpClientName;
private readonly TimeSpan _timeout;

/// <summary>
/// Initializes a new instance of the <see cref="StatusPageHealthCheck"/> class.
/// Initializes a new instance of the <see cref="OpenAIHealthCheck"/> class.
/// </summary>
/// <param name="httpClientFactory">The factory to create HTTP clients.</param>
/// <param name="statusEndpoint">The URI of the status.json endpoint.</param>
/// <param name="resource">The OpenAI resource.</param>
/// <param name="httpClientName">The optional name of the HTTP client to use.</param>
/// <param name="timeout">The optional timeout for the HTTP request.</param>
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<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
// Case 1: Default endpoint - check StatusPage
if (Uri.TryCreate(_resource.Endpoint, UriKind.Absolute, out var endpointUri) &&
Uri.TryCreate(DefaultEndpoint, UriKind.Absolute, out var defaultUri) &&
Uri.Compare(endpointUri, defaultUri, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
{
return await CheckStatusPageAsync(cancellationToken).ConfigureAwait(false);
}

// Case 2: Custom endpoint - return healthy
return await CheckEndpointHealthAsync().ConfigureAwait(false);
}

/// <summary>
/// Checks the StatusPage endpoint for the default OpenAI service.
/// </summary>
private async Task<HealthCheckResult> CheckStatusPageAsync(CancellationToken cancellationToken)
{
var client = string.IsNullOrWhiteSpace(_httpClientName)
? _httpClientFactory.CreateClient()
: _httpClientFactory.CreateClient(_httpClientName);

var statusEndpoint = new Uri("https://status.openai.com/api/v2/status.json");

using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_timeout);

using var req = new HttpRequestMessage(HttpMethod.Get, _statusEndpoint);
using var req = new HttpRequestMessage(HttpMethod.Get, statusEndpoint);
req.Headers.Accept.ParseAdd("application/json");

HttpResponseMessage resp;
Expand Down Expand Up @@ -94,7 +113,7 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
["indicator"] = indicator,
["description"] = description,
["endpoint"] = _statusEndpoint.ToString()
["endpoint"] = statusEndpoint.ToString()
};

// Map indicator -> HealthStatus
Expand All @@ -112,39 +131,12 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
return HealthCheckResult.Unhealthy("Failed to parse StatusPage JSON.", jex);
}
}
}

internal static class StatuspageHealthCheckExtensions
{
/// <summary>
/// Registers a StatusPage health check for a given status.json URL.
/// Returns healthy for custom endpoints.
/// </summary>
public static IDistributedApplicationBuilder AddStatusPageCheck(
this IDistributedApplicationBuilder builder,
string name,
string statusJsonUrl,
string? httpClientName = null,
TimeSpan? timeout = null,
HealthStatus? failureStatus = null,
IEnumerable<string>? tags = null)
private static Task<HealthCheckResult> CheckEndpointHealthAsync()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot just inline this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inlined the method - now directly returns HealthCheckResult.Healthy("Custom OpenAI endpoint configured") in the CheckHealthAsync method instead of calling a separate wrapper method.

Commit: c8a3897

{
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<IHttpClientFactory>();
return new StatusPageHealthCheck(httpFactory, new Uri(statusJsonUrl), httpClientName, timeout);
},
failureStatus: failureStatus,
tags: tags));

return builder;
return Task.FromResult(HealthCheckResult.Healthy("Custom OpenAI endpoint configured"));
}
}
Loading