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; /// /// Validates OpenAI API connectivity. /// public sealed class OpenAiProviderCheck : IDoctorCheck { private const string DefaultModel = "gpt-4o"; private const string DefaultEndpoint = "https://api.openai.com"; /// public string CheckId => "check.ai.provider.openai"; /// public string Name => "OpenAI Provider"; /// public string Description => "Validates OpenAI API connectivity and authentication"; /// public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn; /// public IReadOnlyList Tags => ["ai", "llm", "openai", "gpt", "advisoryai"]; /// public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10); /// public bool CanRun(DoctorPluginContext context) { var apiKey = context.Configuration.GetValue("AdvisoryAI:LlmProviders:OpenAI:ApiKey") ?? Environment.GetEnvironmentVariable("OPENAI_API_KEY"); return !string.IsNullOrWhiteSpace(apiKey); } /// public async Task RunAsync(DoctorPluginContext context, CancellationToken ct) { var result = context.CreateResult(CheckId, "stellaops.doctor.ai", DoctorCategory.AI.ToString()); var apiKey = context.Configuration.GetValue("AdvisoryAI:LlmProviders:OpenAI:ApiKey") ?? Environment.GetEnvironmentVariable("OPENAI_API_KEY"); var endpoint = context.Configuration.GetValue("AdvisoryAI:LlmProviders:OpenAI:Endpoint") ?? DefaultEndpoint; var model = context.Configuration.GetValue("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(); 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(); 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] + "..."; } }