using System; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; namespace Gott.Services { public class OpenAiCheckResult { public string Status { get; set; } // "ok", "invalid", "rate_limited", "quota_exceeded", "error" public string Message { get; set; } public string Organization { get; set; } public string[] Models { get; set; } public int? RateLimitRpm { get; set; } public int? RateLimitTpm { get; set; } } public static class OpenAiKeyChecker { public static async Task CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey) { try { var client = httpFactory.CreateClient(); client.Timeout = TimeSpan.FromSeconds(30); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.openai.com/v1/models"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); var resp = await client.SendAsync(request); var statusCode = (int)resp.StatusCode; if (statusCode == 401) return new OpenAiCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)" }; if (statusCode == 403) return new OpenAiCheckResult { Status = "quota_exceeded", Message = "Keine Berechtigung / Quota erschöpft (403)" }; if (statusCode == 429) { // 429 kann Rate-Limit ODER Quota-Exceeded sein → Body prüfen try { var errBody = await resp.Content.ReadAsStringAsync(); var errDoc = JsonDocument.Parse(errBody); if (errDoc.RootElement.TryGetProperty("error", out var errObj) && errObj.TryGetProperty("code", out var codeProp)) { var code = codeProp.GetString(); if (code == "insufficient_quota") return new OpenAiCheckResult { Status = "quota_exceeded", Message = "Kein Guthaben / Quota erschöpft (429)" }; } } catch { /* Body nicht parsbar → als Rate-Limit behandeln */ } return new OpenAiCheckResult { Status = "rate_limited", Message = "Rate Limited (429)" }; } if (statusCode >= 400) return new OpenAiCheckResult { Status = "error", Message = "API-Fehler (" + statusCode + ")" }; // 200 OK → Key existiert, Modelle extrahieren var body = await resp.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(body); // Organisation aus Header string org = null; if (resp.Headers.TryGetValues("openai-organization", out var orgValues)) org = orgValues.FirstOrDefault(); // Modell-Liste extrahieren (nur die interessantesten) string[] models = Array.Empty(); if (doc.RootElement.TryGetProperty("data", out var data)) { var interestingPrefixes = new[] { "gpt-5", "gpt-4", "gpt-3.5", "o1", "o3", "o4", "chatgpt" }; models = data.EnumerateArray() .Select(m => m.GetProperty("id").GetString()) .Where(id => interestingPrefixes.Any(p => id.StartsWith(p, StringComparison.OrdinalIgnoreCase))) .Distinct() .OrderBy(x => x) .ToArray(); } // Plan-Erkennung anhand der Modelle string plan; if (models.Any(m => m.StartsWith("gpt-5", StringComparison.OrdinalIgnoreCase) || m.StartsWith("o3", StringComparison.OrdinalIgnoreCase) || m.StartsWith("o4", StringComparison.OrdinalIgnoreCase))) plan = "Tier 5+"; else if (models.Any(m => m.StartsWith("gpt-4o", StringComparison.OrdinalIgnoreCase))) plan = "Tier 3+"; else if (models.Any(m => m.StartsWith("gpt-4", StringComparison.OrdinalIgnoreCase))) plan = "Tier 2+"; else if (models.Any(m => m.StartsWith("gpt-3.5", StringComparison.OrdinalIgnoreCase))) plan = "Tier 1"; else plan = "Free"; // Schritt 2: Echten Chat-Call mit max_tokens=1 um Quota zu prüfen // /v1/models gibt 200 auch bei quota_exceeded Keys! var usable = await CheckQuotaAsync(client, apiKey); var msg = plan + " · Org: " + (org ?? "?") + " · " + models.Length + " Modelle"; // Rate-Limits aus dem Chat-Call-Header (falls verfügbar) if (usable.Rpm.HasValue) msg += " · " + usable.Rpm.Value + " RPM"; if (usable.Tpm.HasValue) msg += " · " + usable.Tpm.Value + " TPM"; if (!usable.HasQuota) { var reason = usable.QuotaError == "billing_not_active" ? "Billing inaktiv" : "Kein Guthaben"; return new OpenAiCheckResult { Status = "quota_exceeded", Message = "⚠️ " + msg + " · " + reason, Organization = org, Models = models, RateLimitRpm = usable.Rpm, RateLimitTpm = usable.Tpm }; } return new OpenAiCheckResult { Status = "ok", Message = msg, Organization = org, Models = models, RateLimitRpm = usable.Rpm, RateLimitTpm = usable.Tpm }; } catch (Exception ex) { return new OpenAiCheckResult { Status = "error", Message = ex.Message }; } } private class QuotaResult { public bool HasQuota { get; set; } public string QuotaError { get; set; } public int? Rpm { get; set; } public int? Tpm { get; set; } } private static async Task CheckQuotaAsync(HttpClient client, string apiKey) { try { var chatReq = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions"); chatReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); chatReq.Content = new StringContent( "{\"model\":\"gpt-4o-mini\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":1}", System.Text.Encoding.UTF8, "application/json"); var chatResp = await client.SendAsync(chatReq); var chatStatus = (int)chatResp.StatusCode; int? rpm = null; int? tpm = null; if (chatResp.Headers.TryGetValues("x-ratelimit-limit-requests", out var rpmVals) && int.TryParse(rpmVals.FirstOrDefault(), out var rpmVal)) rpm = rpmVal; if (chatResp.Headers.TryGetValues("x-ratelimit-limit-tokens", out var tpmVals) && int.TryParse(tpmVals.FirstOrDefault(), out var tpmVal)) tpm = tpmVal; if (chatStatus == 429) { try { var errBody = await chatResp.Content.ReadAsStringAsync(); var errDoc = JsonDocument.Parse(errBody); if (errDoc.RootElement.TryGetProperty("error", out var errObj) && errObj.TryGetProperty("code", out var codeProp) ) { var code = codeProp.GetString(); if (code == "insufficient_quota" || code == "billing_not_active") { return new QuotaResult { HasQuota = false, QuotaError = code, Rpm = rpm, Tpm = tpm }; } } } catch { } // Echtes Rate-Limit → Key hat Quota, ist nur temporär blockiert return new QuotaResult { HasQuota = true, Rpm = rpm, Tpm = tpm }; } // 200 = funktioniert, alles andere = vermutlich trotzdem nutzbar return new QuotaResult { HasQuota = chatStatus < 400, Rpm = rpm, Tpm = tpm }; } catch { // Bei Netzwerkfehler etc. → optimistisch annehmen return new QuotaResult { HasQuota = true }; } } } }