-
Couldn't load subscription status.
- Fork 712
Adapt OpenAI health check based on endpoint configuration #11763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
dd4a89f
b5f8fda
4cae8ed
1399972
54f5e80
b00e489
4d2152b
df2df9b
b61040e
8050e74
c8a3897
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,10 @@ | ||||||||||||||||
| // 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; | ||||||||||||||||
|
|
@@ -28,17 +31,20 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context | |||||||||||||||
|
|
||||||||||||||||
| try | ||||||||||||||||
| { | ||||||||||||||||
| // Case 1: Default endpoint - use StatusPageHealthCheck | ||||||||||||||||
| // Case 1: Default endpoint - use StatusPage check | ||||||||||||||||
| if (resource.Endpoint == DefaultEndpoint) | ||||||||||||||||
| { | ||||||||||||||||
| return await CheckStatusPageAsync(cancellationToken).ConfigureAwait(false); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Case 2: Custom endpoint without model health check - return healthy | ||||||||||||||||
| // We can't check the endpoint without a model, so we just return healthy | ||||||||||||||||
| // The model-level health check will do the actual verification if WithHealthCheck is called | ||||||||||||||||
| _result = HealthCheckResult.Healthy("Custom OpenAI endpoint configured"); | ||||||||||||||||
| return _result.Value; | ||||||||||||||||
| // Case 2: Custom endpoint with model health check - use model health check | ||||||||||||||||
| if (resource.UseModelHealthCheck && resource.ModelConnectionString is not null) | ||||||||||||||||
| { | ||||||||||||||||
| return await CheckModelHealthAsync(cancellationToken).ConfigureAwait(false); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Case 3: Custom endpoint without model health check - return healthy | ||||||||||||||||
| return await CheckEndpointHealthAsync().ConfigureAwait(false); | ||||||||||||||||
| } | ||||||||||||||||
| catch (Exception ex) | ||||||||||||||||
| { | ||||||||||||||||
|
|
@@ -47,6 +53,9 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// <summary> | ||||||||||||||||
| /// Checks the StatusPage endpoint for the default OpenAI service. | ||||||||||||||||
| /// </summary> | ||||||||||||||||
| private async Task<HealthCheckResult> CheckStatusPageAsync(CancellationToken cancellationToken) | ||||||||||||||||
| { | ||||||||||||||||
| var client = httpClientFactory.CreateClient("OpenAIHealthCheck"); | ||||||||||||||||
|
|
@@ -86,6 +95,11 @@ private async Task<HealthCheckResult> CheckStatusPageAsync(CancellationToken can | |||||||||||||||
| using var stream = await resp.Content.ReadAsStreamAsync(cts.Token).ConfigureAwait(false); | ||||||||||||||||
| using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cts.Token).ConfigureAwait(false); | ||||||||||||||||
|
|
||||||||||||||||
| // Expected shape: | ||||||||||||||||
| // { | ||||||||||||||||
| // "page": { ... }, | ||||||||||||||||
| // "status": { "indicator": "none|minor|major|critical", "description": "..." } | ||||||||||||||||
| // } | ||||||||||||||||
| if (!doc.RootElement.TryGetProperty("status", out var statusEl)) | ||||||||||||||||
| { | ||||||||||||||||
| _result = HealthCheckResult.Unhealthy("Missing 'status' object in StatusPage response."); | ||||||||||||||||
|
|
@@ -107,6 +121,7 @@ private async Task<HealthCheckResult> CheckStatusPageAsync(CancellationToken can | |||||||||||||||
| ["endpoint"] = statusEndpoint.ToString() | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| // Map indicator -> HealthStatus | ||||||||||||||||
| _result = indicator switch | ||||||||||||||||
| { | ||||||||||||||||
| "none" => HealthCheckResult.Healthy(description.Length > 0 ? description : "All systems operational."), | ||||||||||||||||
|
|
@@ -124,4 +139,105 @@ private async Task<HealthCheckResult> CheckStatusPageAsync(CancellationToken can | |||||||||||||||
| return _result.Value; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// <summary> | ||||||||||||||||
| /// Returns healthy for custom endpoints when no model health check is configured. | ||||||||||||||||
| /// </summary> | ||||||||||||||||
| private Task<HealthCheckResult> CheckEndpointHealthAsync() | ||||||||||||||||
| { | ||||||||||||||||
| _result = HealthCheckResult.Healthy("Custom OpenAI endpoint configured"); | ||||||||||||||||
| return Task.FromResult(_result.Value); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /// <summary> | ||||||||||||||||
| /// Checks the health of the OpenAI endpoint by sending a test request to the model endpoint. | ||||||||||||||||
| /// </summary> | ||||||||||||||||
| private async Task<HealthCheckResult> CheckModelHealthAsync(CancellationToken cancellationToken) | ||||||||||||||||
| { | ||||||||||||||||
| var httpClient = httpClientFactory.CreateClient("OpenAIHealthCheck"); | ||||||||||||||||
| var connectionString = resource.ModelConnectionString; | ||||||||||||||||
|
|
||||||||||||||||
| if (connectionString is null) | ||||||||||||||||
| { | ||||||||||||||||
| _result = HealthCheckResult.Unhealthy("Model connection string not available"); | ||||||||||||||||
| return _result.Value; | ||||||||||||||||
| } | ||||||||||||||||
|
||||||||||||||||
| var connectionString = resource.ModelConnectionString; | |
| if (connectionString is null) | |
| { | |
| _result = HealthCheckResult.Unhealthy("Model connection string not available"); | |
| return _result.Value; | |
| } |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| var builder = new DbConnectionStringBuilder() { ConnectionString = await connectionString().ConfigureAwait(false) }; | |
| var builder = new DbConnectionStringBuilder() { ConnectionString = await resource.ConnectionStringExpression.GetValueAsync(cancellationToken).ConfigureAwait(false) }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,16 @@ public sealed class OpenAIResource : Resource, IResourceWithConnectionString | |
| /// </summary> | ||
| public string Endpoint { get; internal set; } = DefaultEndpoint; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets whether a model health check is enabled for this resource. | ||
| /// </summary> | ||
| internal bool UseModelHealthCheck { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the connection string provider for model health check. | ||
| /// </summary> | ||
| internal Func<ValueTask<string?>>? ModelConnectionString { get; set; } | ||
|
||
|
|
||
| /// <summary> | ||
| /// Creates a new <see cref="OpenAIResource"/>. | ||
| /// </summary> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.