189 lines
6.7 KiB
C#
189 lines
6.7 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StellaOps.Doctor.Models;
|
|
using StellaOps.Doctor.Plugins;
|
|
|
|
namespace StellaOps.Doctor.Plugins.AI.Checks;
|
|
|
|
/// <summary>
|
|
/// Validates OpenAI API connectivity.
|
|
/// </summary>
|
|
public sealed class OpenAiProviderCheck : IDoctorCheck
|
|
{
|
|
private const string DefaultModel = "gpt-4o";
|
|
private const string DefaultEndpoint = "https://api.openai.com";
|
|
|
|
/// <inheritdoc />
|
|
public string CheckId => "check.ai.provider.openai";
|
|
|
|
/// <inheritdoc />
|
|
public string Name => "OpenAI Provider";
|
|
|
|
/// <inheritdoc />
|
|
public string Description => "Validates OpenAI API connectivity and authentication";
|
|
|
|
/// <inheritdoc />
|
|
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<string> Tags => ["ai", "llm", "openai", "gpt", "advisoryai"];
|
|
|
|
/// <inheritdoc />
|
|
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
|
|
|
|
/// <inheritdoc />
|
|
public bool CanRun(DoctorPluginContext context)
|
|
{
|
|
var apiKey = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:OpenAI:ApiKey")
|
|
?? Environment.GetEnvironmentVariable("OPENAI_API_KEY");
|
|
|
|
return !string.IsNullOrWhiteSpace(apiKey);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
|
{
|
|
var result = context.CreateResult(CheckId, "stellaops.doctor.ai", DoctorCategory.AI.ToString());
|
|
|
|
var apiKey = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:OpenAI:ApiKey")
|
|
?? Environment.GetEnvironmentVariable("OPENAI_API_KEY");
|
|
|
|
var endpoint = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:OpenAI:Endpoint")
|
|
?? DefaultEndpoint;
|
|
|
|
var model = context.Configuration.GetValue<string>("AdvisoryAI:LlmProviders:OpenAI:Model")
|
|
?? DefaultModel;
|
|
|
|
if (string.IsNullOrWhiteSpace(apiKey))
|
|
{
|
|
return result
|
|
.Skip("OpenAI API key not configured")
|
|
.WithEvidence("OpenAI provider", e =>
|
|
{
|
|
e.Add("Endpoint", endpoint);
|
|
e.Add("ApiKeyConfigured", "false");
|
|
})
|
|
.Build();
|
|
}
|
|
|
|
var httpClientFactory = context.Services.GetService<IHttpClientFactory>();
|
|
if (httpClientFactory == null)
|
|
{
|
|
return result
|
|
.Skip("HttpClientFactory not available")
|
|
.WithEvidence("OpenAI provider", e =>
|
|
{
|
|
e.Add("Endpoint", endpoint);
|
|
e.Add("Error", "IHttpClientFactory not registered");
|
|
})
|
|
.Build();
|
|
}
|
|
|
|
try
|
|
{
|
|
using var client = httpClientFactory.CreateClient();
|
|
client.Timeout = TimeSpan.FromSeconds(10);
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
|
|
|
// List models to validate API key (lightweight call)
|
|
using var response = await client.GetAsync($"{endpoint}/v1/models", ct);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
return result
|
|
.Pass("OpenAI API is accessible")
|
|
.WithEvidence("OpenAI provider", e =>
|
|
{
|
|
e.Add("Endpoint", endpoint);
|
|
e.Add("Model", model);
|
|
e.Add("ApiKeyConfigured", "true (masked)");
|
|
e.Add("StatusCode", ((int)response.StatusCode).ToString());
|
|
})
|
|
.Build();
|
|
}
|
|
|
|
var errorBody = await response.Content.ReadAsStringAsync(ct);
|
|
var statusCode = (int)response.StatusCode;
|
|
|
|
var issues = new List<string>();
|
|
if (statusCode == 401)
|
|
{
|
|
issues.Add("Invalid API key");
|
|
}
|
|
else if (statusCode == 403)
|
|
{
|
|
issues.Add("Access forbidden - check API key permissions");
|
|
}
|
|
else if (statusCode == 429)
|
|
{
|
|
issues.Add("Rate limited - too many requests");
|
|
}
|
|
else
|
|
{
|
|
issues.Add($"API returned status {statusCode}");
|
|
}
|
|
|
|
return result
|
|
.Warn($"OpenAI API issue: {response.StatusCode}")
|
|
.WithEvidence("OpenAI provider", e =>
|
|
{
|
|
e.Add("Endpoint", endpoint);
|
|
e.Add("Model", model);
|
|
e.Add("StatusCode", statusCode.ToString());
|
|
e.Add("Error", TruncateError(errorBody));
|
|
})
|
|
.WithCauses(issues.ToArray())
|
|
.WithRemediation(r => r
|
|
.AddManualStep(1, "Verify API key", "Check OPENAI_API_KEY is valid")
|
|
.AddManualStep(2, "Check quotas", "Verify API usage limits on platform.openai.com"))
|
|
.WithVerification("stella doctor --check check.ai.provider.openai")
|
|
.Build();
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return result
|
|
.Fail($"Cannot connect to OpenAI API: {ex.Message}")
|
|
.WithEvidence("OpenAI provider", e =>
|
|
{
|
|
e.Add("Endpoint", endpoint);
|
|
e.Add("Error", ex.Message);
|
|
})
|
|
.WithCauses("Network connectivity issue or invalid endpoint")
|
|
.WithRemediation(r => r
|
|
.AddManualStep(1, "Check network", "Verify network connectivity to api.openai.com")
|
|
.AddManualStep(2, "Check proxy", "Ensure proxy settings are configured if required"))
|
|
.WithVerification("stella doctor --check check.ai.provider.openai")
|
|
.Build();
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
return result
|
|
.Fail($"OpenAI API error: {ex.Message}")
|
|
.WithEvidence("OpenAI provider", e =>
|
|
{
|
|
e.Add("Endpoint", endpoint);
|
|
e.Add("Error", ex.GetType().Name);
|
|
})
|
|
.Build();
|
|
}
|
|
}
|
|
|
|
private static string TruncateError(string error, int maxLength = 200)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(error))
|
|
{
|
|
return "(empty)";
|
|
}
|
|
|
|
if (error.Length <= maxLength)
|
|
{
|
|
return error;
|
|
}
|
|
|
|
return error[..maxLength] + "...";
|
|
}
|
|
}
|